Source file
src/runtime/metrics_test.go
1
2
3
4
5 package runtime_test
6
7 import (
8 "runtime"
9 "runtime/metrics"
10 "sort"
11 "strings"
12 "testing"
13 "time"
14 "unsafe"
15 )
16
17 func prepareAllMetricsSamples() (map[string]metrics.Description, []metrics.Sample) {
18 all := metrics.All()
19 samples := make([]metrics.Sample, len(all))
20 descs := make(map[string]metrics.Description)
21 for i := range all {
22 samples[i].Name = all[i].Name
23 descs[all[i].Name] = all[i]
24 }
25 return descs, samples
26 }
27
28 func TestReadMetrics(t *testing.T) {
29
30
31 var mstats runtime.MemStats
32 _, samples := prepareAllMetricsSamples()
33 runtime.ReadMetricsSlow(&mstats, unsafe.Pointer(&samples[0]), len(samples), cap(samples))
34
35 checkUint64 := func(t *testing.T, m string, got, want uint64) {
36 t.Helper()
37 if got != want {
38 t.Errorf("metric %q: got %d, want %d", m, got, want)
39 }
40 }
41
42
43 var allocsBySize *metrics.Float64Histogram
44 var tinyAllocs uint64
45 var mallocs, frees uint64
46 for i := range samples {
47 switch name := samples[i].Name; name {
48 case "/memory/classes/heap/free:bytes":
49 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapIdle-mstats.HeapReleased)
50 case "/memory/classes/heap/released:bytes":
51 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapReleased)
52 case "/memory/classes/heap/objects:bytes":
53 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapAlloc)
54 case "/memory/classes/heap/unused:bytes":
55 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapInuse-mstats.HeapAlloc)
56 case "/memory/classes/heap/stacks:bytes":
57 checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackInuse)
58 case "/memory/classes/metadata/mcache/free:bytes":
59 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheSys-mstats.MCacheInuse)
60 case "/memory/classes/metadata/mcache/inuse:bytes":
61 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheInuse)
62 case "/memory/classes/metadata/mspan/free:bytes":
63 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanSys-mstats.MSpanInuse)
64 case "/memory/classes/metadata/mspan/inuse:bytes":
65 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanInuse)
66 case "/memory/classes/metadata/other:bytes":
67 checkUint64(t, name, samples[i].Value.Uint64(), mstats.GCSys)
68 case "/memory/classes/os-stacks:bytes":
69 checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackSys-mstats.StackInuse)
70 case "/memory/classes/other:bytes":
71 checkUint64(t, name, samples[i].Value.Uint64(), mstats.OtherSys)
72 case "/memory/classes/profiling/buckets:bytes":
73 checkUint64(t, name, samples[i].Value.Uint64(), mstats.BuckHashSys)
74 case "/memory/classes/total:bytes":
75 checkUint64(t, name, samples[i].Value.Uint64(), mstats.Sys)
76 case "/gc/heap/allocs-by-size:bytes":
77 hist := samples[i].Value.Float64Histogram()
78
79
80 for i, sc := range mstats.BySize[1:] {
81 if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
82 t.Errorf("bucket does not match size class: got %f, want %f", b, s)
83
84 continue
85 }
86 if c, m := hist.Counts[i], sc.Mallocs; c != m {
87 t.Errorf("histogram counts do not much BySize for class %d: got %d, want %d", i, c, m)
88 }
89 }
90 allocsBySize = hist
91 case "/gc/heap/allocs:bytes":
92 checkUint64(t, name, samples[i].Value.Uint64(), mstats.TotalAlloc)
93 case "/gc/heap/frees-by-size:bytes":
94 hist := samples[i].Value.Float64Histogram()
95
96
97 for i, sc := range mstats.BySize[1:] {
98 if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
99 t.Errorf("bucket does not match size class: got %f, want %f", b, s)
100
101 continue
102 }
103 if c, f := hist.Counts[i], sc.Frees; c != f {
104 t.Errorf("histogram counts do not match BySize for class %d: got %d, want %d", i, c, f)
105 }
106 }
107 case "/gc/heap/frees:bytes":
108 checkUint64(t, name, samples[i].Value.Uint64(), mstats.TotalAlloc-mstats.HeapAlloc)
109 case "/gc/heap/tiny/allocs:objects":
110
111
112
113
114
115
116
117
118
119 tinyAllocs = samples[i].Value.Uint64()
120
121
122
123 case "/gc/heap/allocs:objects":
124 mallocs = samples[i].Value.Uint64()
125 case "/gc/heap/frees:objects":
126 frees = samples[i].Value.Uint64()
127 case "/gc/heap/objects:objects":
128 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapObjects)
129 case "/gc/heap/goal:bytes":
130 checkUint64(t, name, samples[i].Value.Uint64(), mstats.NextGC)
131 case "/gc/cycles/automatic:gc-cycles":
132 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumGC-mstats.NumForcedGC))
133 case "/gc/cycles/forced:gc-cycles":
134 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumForcedGC))
135 case "/gc/cycles/total:gc-cycles":
136 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumGC))
137 }
138 }
139
140
141 nonTinyAllocs := uint64(0)
142 for _, c := range allocsBySize.Counts {
143 nonTinyAllocs += c
144 }
145 checkUint64(t, "/gc/heap/tiny/allocs:objects", tinyAllocs, mstats.Mallocs-nonTinyAllocs)
146
147
148 checkUint64(t, "/gc/heap/allocs:objects", mallocs, mstats.Mallocs-tinyAllocs)
149 checkUint64(t, "/gc/heap/frees:objects", frees, mstats.Frees-tinyAllocs)
150 }
151
152 func TestReadMetricsConsistency(t *testing.T) {
153
154
155
156
157
158
159 runtime.GC()
160 runtime.GC()
161 runtime.GC()
162
163
164 descs, samples := prepareAllMetricsSamples()
165 metrics.Read(samples)
166
167
168 var totalVirtual struct {
169 got, want uint64
170 }
171 var objects struct {
172 alloc, free *metrics.Float64Histogram
173 allocs, frees uint64
174 allocdBytes, freedBytes uint64
175 total, totalBytes uint64
176 }
177 var gc struct {
178 numGC uint64
179 pauses uint64
180 }
181 for i := range samples {
182 kind := samples[i].Value.Kind()
183 if want := descs[samples[i].Name].Kind; kind != want {
184 t.Errorf("supported metric %q has unexpected kind: got %d, want %d", samples[i].Name, kind, want)
185 continue
186 }
187 if samples[i].Name != "/memory/classes/total:bytes" && strings.HasPrefix(samples[i].Name, "/memory/classes") {
188 v := samples[i].Value.Uint64()
189 totalVirtual.want += v
190
191
192
193
194 if int64(v) < 0 {
195 t.Errorf("%q has high/negative value: %d", samples[i].Name, v)
196 }
197 }
198 switch samples[i].Name {
199 case "/memory/classes/total:bytes":
200 totalVirtual.got = samples[i].Value.Uint64()
201 case "/memory/classes/heap/objects:bytes":
202 objects.totalBytes = samples[i].Value.Uint64()
203 case "/gc/heap/objects:objects":
204 objects.total = samples[i].Value.Uint64()
205 case "/gc/heap/allocs:bytes":
206 objects.allocdBytes = samples[i].Value.Uint64()
207 case "/gc/heap/allocs:objects":
208 objects.allocs = samples[i].Value.Uint64()
209 case "/gc/heap/allocs-by-size:bytes":
210 objects.alloc = samples[i].Value.Float64Histogram()
211 case "/gc/heap/frees:bytes":
212 objects.freedBytes = samples[i].Value.Uint64()
213 case "/gc/heap/frees:objects":
214 objects.frees = samples[i].Value.Uint64()
215 case "/gc/heap/frees-by-size:bytes":
216 objects.free = samples[i].Value.Float64Histogram()
217 case "/gc/cycles:gc-cycles":
218 gc.numGC = samples[i].Value.Uint64()
219 case "/gc/pauses:seconds":
220 h := samples[i].Value.Float64Histogram()
221 gc.pauses = 0
222 for i := range h.Counts {
223 gc.pauses += h.Counts[i]
224 }
225 case "/sched/goroutines:goroutines":
226 if samples[i].Value.Uint64() < 1 {
227 t.Error("number of goroutines is less than one")
228 }
229 }
230 }
231 if totalVirtual.got != totalVirtual.want {
232 t.Errorf(`"/memory/classes/total:bytes" does not match sum of /memory/classes/**: got %d, want %d`, totalVirtual.got, totalVirtual.want)
233 }
234 if got, want := objects.allocs-objects.frees, objects.total; got != want {
235 t.Errorf("mismatch between object alloc/free tallies and total: got %d, want %d", got, want)
236 }
237 if got, want := objects.allocdBytes-objects.freedBytes, objects.totalBytes; got != want {
238 t.Errorf("mismatch between object alloc/free tallies and total: got %d, want %d", got, want)
239 }
240 if b, c := len(objects.alloc.Buckets), len(objects.alloc.Counts); b != c+1 {
241 t.Errorf("allocs-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
242 }
243 if b, c := len(objects.free.Buckets), len(objects.free.Counts); b != c+1 {
244 t.Errorf("frees-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
245 }
246 if len(objects.alloc.Buckets) != len(objects.free.Buckets) {
247 t.Error("allocs-by-size and frees-by-size buckets don't match in length")
248 } else if len(objects.alloc.Counts) != len(objects.free.Counts) {
249 t.Error("allocs-by-size and frees-by-size counts don't match in length")
250 } else {
251 for i := range objects.alloc.Buckets {
252 ba := objects.alloc.Buckets[i]
253 bf := objects.free.Buckets[i]
254 if ba != bf {
255 t.Errorf("bucket %d is different for alloc and free hists: %f != %f", i, ba, bf)
256 }
257 }
258 if !t.Failed() {
259 var gotAlloc, gotFree uint64
260 want := objects.total
261 for i := range objects.alloc.Counts {
262 if objects.alloc.Counts[i] < objects.free.Counts[i] {
263 t.Errorf("found more allocs than frees in object dist bucket %d", i)
264 continue
265 }
266 gotAlloc += objects.alloc.Counts[i]
267 gotFree += objects.free.Counts[i]
268 }
269 if got := gotAlloc - gotFree; got != want {
270 t.Errorf("object distribution counts don't match count of live objects: got %d, want %d", got, want)
271 }
272 if gotAlloc != objects.allocs {
273 t.Errorf("object distribution counts don't match total allocs: got %d, want %d", gotAlloc, objects.allocs)
274 }
275 if gotFree != objects.frees {
276 t.Errorf("object distribution counts don't match total allocs: got %d, want %d", gotFree, objects.frees)
277 }
278 }
279 }
280
281
282 if gc.pauses < gc.numGC*2 {
283 t.Errorf("fewer pauses than expected: got %d, want at least %d", gc.pauses, gc.numGC*2)
284 }
285 }
286
287 func BenchmarkReadMetricsLatency(b *testing.B) {
288 stop := applyGCLoad(b)
289
290
291 latencies := make([]time.Duration, 0, 1024)
292 _, samples := prepareAllMetricsSamples()
293
294
295 b.ResetTimer()
296 for i := 0; i < b.N; i++ {
297 start := time.Now()
298 metrics.Read(samples)
299 latencies = append(latencies, time.Now().Sub(start))
300 }
301
302
303
304 b.StopTimer()
305 stop()
306
307
308
309
310 b.ReportMetric(0, "ns/op")
311 b.ReportMetric(0, "B/op")
312 b.ReportMetric(0, "allocs/op")
313
314
315 sort.Slice(latencies, func(i, j int) bool {
316 return latencies[i] < latencies[j]
317 })
318 b.ReportMetric(float64(latencies[len(latencies)*50/100]), "p50-ns")
319 b.ReportMetric(float64(latencies[len(latencies)*90/100]), "p90-ns")
320 b.ReportMetric(float64(latencies[len(latencies)*99/100]), "p99-ns")
321 }
322
View as plain text