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 "sync"
13 "testing"
14 "time"
15 "unsafe"
16 )
17
18 func prepareAllMetricsSamples() (map[string]metrics.Description, []metrics.Sample) {
19 all := metrics.All()
20 samples := make([]metrics.Sample, len(all))
21 descs := make(map[string]metrics.Description)
22 for i := range all {
23 samples[i].Name = all[i].Name
24 descs[all[i].Name] = all[i]
25 }
26 return descs, samples
27 }
28
29 func TestReadMetrics(t *testing.T) {
30
31
32 var mstats runtime.MemStats
33 _, samples := prepareAllMetricsSamples()
34 runtime.ReadMetricsSlow(&mstats, unsafe.Pointer(&samples[0]), len(samples), cap(samples))
35
36 checkUint64 := func(t *testing.T, m string, got, want uint64) {
37 t.Helper()
38 if got != want {
39 t.Errorf("metric %q: got %d, want %d", m, got, want)
40 }
41 }
42
43
44 var allocsBySize *metrics.Float64Histogram
45 var tinyAllocs uint64
46 var mallocs, frees uint64
47 for i := range samples {
48 switch name := samples[i].Name; name {
49 case "/cgo/go-to-c-calls:calls":
50 checkUint64(t, name, samples[i].Value.Uint64(), uint64(runtime.NumCgoCall()))
51 case "/memory/classes/heap/free:bytes":
52 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapIdle-mstats.HeapReleased)
53 case "/memory/classes/heap/released:bytes":
54 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapReleased)
55 case "/memory/classes/heap/objects:bytes":
56 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapAlloc)
57 case "/memory/classes/heap/unused:bytes":
58 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapInuse-mstats.HeapAlloc)
59 case "/memory/classes/heap/stacks:bytes":
60 checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackInuse)
61 case "/memory/classes/metadata/mcache/free:bytes":
62 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheSys-mstats.MCacheInuse)
63 case "/memory/classes/metadata/mcache/inuse:bytes":
64 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheInuse)
65 case "/memory/classes/metadata/mspan/free:bytes":
66 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanSys-mstats.MSpanInuse)
67 case "/memory/classes/metadata/mspan/inuse:bytes":
68 checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanInuse)
69 case "/memory/classes/metadata/other:bytes":
70 checkUint64(t, name, samples[i].Value.Uint64(), mstats.GCSys)
71 case "/memory/classes/os-stacks:bytes":
72 checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackSys-mstats.StackInuse)
73 case "/memory/classes/other:bytes":
74 checkUint64(t, name, samples[i].Value.Uint64(), mstats.OtherSys)
75 case "/memory/classes/profiling/buckets:bytes":
76 checkUint64(t, name, samples[i].Value.Uint64(), mstats.BuckHashSys)
77 case "/memory/classes/total:bytes":
78 checkUint64(t, name, samples[i].Value.Uint64(), mstats.Sys)
79 case "/gc/heap/allocs-by-size:bytes":
80 hist := samples[i].Value.Float64Histogram()
81
82
83 for i, sc := range mstats.BySize[1:] {
84 if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
85 t.Errorf("bucket does not match size class: got %f, want %f", b, s)
86
87 continue
88 }
89 if c, m := hist.Counts[i], sc.Mallocs; c != m {
90 t.Errorf("histogram counts do not much BySize for class %d: got %d, want %d", i, c, m)
91 }
92 }
93 allocsBySize = hist
94 case "/gc/heap/allocs:bytes":
95 checkUint64(t, name, samples[i].Value.Uint64(), mstats.TotalAlloc)
96 case "/gc/heap/frees-by-size:bytes":
97 hist := samples[i].Value.Float64Histogram()
98
99
100 for i, sc := range mstats.BySize[1:] {
101 if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
102 t.Errorf("bucket does not match size class: got %f, want %f", b, s)
103
104 continue
105 }
106 if c, f := hist.Counts[i], sc.Frees; c != f {
107 t.Errorf("histogram counts do not match BySize for class %d: got %d, want %d", i, c, f)
108 }
109 }
110 case "/gc/heap/frees:bytes":
111 checkUint64(t, name, samples[i].Value.Uint64(), mstats.TotalAlloc-mstats.HeapAlloc)
112 case "/gc/heap/tiny/allocs:objects":
113
114
115
116
117
118
119
120
121
122 tinyAllocs = samples[i].Value.Uint64()
123
124
125
126 case "/gc/heap/allocs:objects":
127 mallocs = samples[i].Value.Uint64()
128 case "/gc/heap/frees:objects":
129 frees = samples[i].Value.Uint64()
130 case "/gc/heap/objects:objects":
131 checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapObjects)
132 case "/gc/heap/goal:bytes":
133 checkUint64(t, name, samples[i].Value.Uint64(), mstats.NextGC)
134 case "/gc/cycles/automatic:gc-cycles":
135 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumGC-mstats.NumForcedGC))
136 case "/gc/cycles/forced:gc-cycles":
137 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumForcedGC))
138 case "/gc/cycles/total:gc-cycles":
139 checkUint64(t, name, samples[i].Value.Uint64(), uint64(mstats.NumGC))
140 }
141 }
142
143
144 nonTinyAllocs := uint64(0)
145 for _, c := range allocsBySize.Counts {
146 nonTinyAllocs += c
147 }
148 checkUint64(t, "/gc/heap/tiny/allocs:objects", tinyAllocs, mstats.Mallocs-nonTinyAllocs)
149
150
151 checkUint64(t, "/gc/heap/allocs:objects", mallocs, mstats.Mallocs-tinyAllocs)
152 checkUint64(t, "/gc/heap/frees:objects", frees, mstats.Frees-tinyAllocs)
153 }
154
155 func TestReadMetricsConsistency(t *testing.T) {
156
157
158
159
160
161
162 runtime.GC()
163 runtime.GC()
164 runtime.GC()
165
166
167 descs, samples := prepareAllMetricsSamples()
168 metrics.Read(samples)
169
170
171 var totalVirtual struct {
172 got, want uint64
173 }
174 var objects struct {
175 alloc, free *metrics.Float64Histogram
176 allocs, frees uint64
177 allocdBytes, freedBytes uint64
178 total, totalBytes uint64
179 }
180 var gc struct {
181 numGC uint64
182 pauses uint64
183 }
184 for i := range samples {
185 kind := samples[i].Value.Kind()
186 if want := descs[samples[i].Name].Kind; kind != want {
187 t.Errorf("supported metric %q has unexpected kind: got %d, want %d", samples[i].Name, kind, want)
188 continue
189 }
190 if samples[i].Name != "/memory/classes/total:bytes" && strings.HasPrefix(samples[i].Name, "/memory/classes") {
191 v := samples[i].Value.Uint64()
192 totalVirtual.want += v
193
194
195
196
197 if int64(v) < 0 {
198 t.Errorf("%q has high/negative value: %d", samples[i].Name, v)
199 }
200 }
201 switch samples[i].Name {
202 case "/memory/classes/total:bytes":
203 totalVirtual.got = samples[i].Value.Uint64()
204 case "/memory/classes/heap/objects:bytes":
205 objects.totalBytes = samples[i].Value.Uint64()
206 case "/gc/heap/objects:objects":
207 objects.total = samples[i].Value.Uint64()
208 case "/gc/heap/allocs:bytes":
209 objects.allocdBytes = samples[i].Value.Uint64()
210 case "/gc/heap/allocs:objects":
211 objects.allocs = samples[i].Value.Uint64()
212 case "/gc/heap/allocs-by-size:bytes":
213 objects.alloc = samples[i].Value.Float64Histogram()
214 case "/gc/heap/frees:bytes":
215 objects.freedBytes = samples[i].Value.Uint64()
216 case "/gc/heap/frees:objects":
217 objects.frees = samples[i].Value.Uint64()
218 case "/gc/heap/frees-by-size:bytes":
219 objects.free = samples[i].Value.Float64Histogram()
220 case "/gc/cycles:gc-cycles":
221 gc.numGC = samples[i].Value.Uint64()
222 case "/gc/pauses:seconds":
223 h := samples[i].Value.Float64Histogram()
224 gc.pauses = 0
225 for i := range h.Counts {
226 gc.pauses += h.Counts[i]
227 }
228 case "/sched/gomaxprocs:threads":
229 if got, want := samples[i].Value.Uint64(), uint64(runtime.GOMAXPROCS(-1)); got != want {
230 t.Errorf("gomaxprocs doesn't match runtime.GOMAXPROCS: got %d, want %d", got, want)
231 }
232 case "/sched/goroutines:goroutines":
233 if samples[i].Value.Uint64() < 1 {
234 t.Error("number of goroutines is less than one")
235 }
236 }
237 }
238 if totalVirtual.got != totalVirtual.want {
239 t.Errorf(`"/memory/classes/total:bytes" does not match sum of /memory/classes/**: got %d, want %d`, totalVirtual.got, totalVirtual.want)
240 }
241 if got, want := objects.allocs-objects.frees, objects.total; got != want {
242 t.Errorf("mismatch between object alloc/free tallies and total: got %d, want %d", got, want)
243 }
244 if got, want := objects.allocdBytes-objects.freedBytes, objects.totalBytes; got != want {
245 t.Errorf("mismatch between object alloc/free tallies and total: got %d, want %d", got, want)
246 }
247 if b, c := len(objects.alloc.Buckets), len(objects.alloc.Counts); b != c+1 {
248 t.Errorf("allocs-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
249 }
250 if b, c := len(objects.free.Buckets), len(objects.free.Counts); b != c+1 {
251 t.Errorf("frees-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
252 }
253 if len(objects.alloc.Buckets) != len(objects.free.Buckets) {
254 t.Error("allocs-by-size and frees-by-size buckets don't match in length")
255 } else if len(objects.alloc.Counts) != len(objects.free.Counts) {
256 t.Error("allocs-by-size and frees-by-size counts don't match in length")
257 } else {
258 for i := range objects.alloc.Buckets {
259 ba := objects.alloc.Buckets[i]
260 bf := objects.free.Buckets[i]
261 if ba != bf {
262 t.Errorf("bucket %d is different for alloc and free hists: %f != %f", i, ba, bf)
263 }
264 }
265 if !t.Failed() {
266 var gotAlloc, gotFree uint64
267 want := objects.total
268 for i := range objects.alloc.Counts {
269 if objects.alloc.Counts[i] < objects.free.Counts[i] {
270 t.Errorf("found more allocs than frees in object dist bucket %d", i)
271 continue
272 }
273 gotAlloc += objects.alloc.Counts[i]
274 gotFree += objects.free.Counts[i]
275 }
276 if got := gotAlloc - gotFree; got != want {
277 t.Errorf("object distribution counts don't match count of live objects: got %d, want %d", got, want)
278 }
279 if gotAlloc != objects.allocs {
280 t.Errorf("object distribution counts don't match total allocs: got %d, want %d", gotAlloc, objects.allocs)
281 }
282 if gotFree != objects.frees {
283 t.Errorf("object distribution counts don't match total allocs: got %d, want %d", gotFree, objects.frees)
284 }
285 }
286 }
287
288
289 if gc.pauses < gc.numGC*2 {
290 t.Errorf("fewer pauses than expected: got %d, want at least %d", gc.pauses, gc.numGC*2)
291 }
292 }
293
294 func BenchmarkReadMetricsLatency(b *testing.B) {
295 stop := applyGCLoad(b)
296
297
298 latencies := make([]time.Duration, 0, 1024)
299 _, samples := prepareAllMetricsSamples()
300
301
302 b.ResetTimer()
303 for i := 0; i < b.N; i++ {
304 start := time.Now()
305 metrics.Read(samples)
306 latencies = append(latencies, time.Now().Sub(start))
307 }
308
309
310
311 b.StopTimer()
312 stop()
313
314
315
316
317 b.ReportMetric(0, "ns/op")
318 b.ReportMetric(0, "B/op")
319 b.ReportMetric(0, "allocs/op")
320
321
322 sort.Slice(latencies, func(i, j int) bool {
323 return latencies[i] < latencies[j]
324 })
325 b.ReportMetric(float64(latencies[len(latencies)*50/100]), "p50-ns")
326 b.ReportMetric(float64(latencies[len(latencies)*90/100]), "p90-ns")
327 b.ReportMetric(float64(latencies[len(latencies)*99/100]), "p99-ns")
328 }
329
330 var readMetricsSink [1024]interface{}
331
332 func TestReadMetricsCumulative(t *testing.T) {
333
334 descs := metrics.All()
335 var samples [2][]metrics.Sample
336 samples[0] = make([]metrics.Sample, len(descs))
337 samples[1] = make([]metrics.Sample, len(descs))
338 total := 0
339 for i := range samples[0] {
340 if !descs[i].Cumulative {
341 continue
342 }
343 samples[0][total].Name = descs[i].Name
344 total++
345 }
346 samples[0] = samples[0][:total]
347 samples[1] = samples[1][:total]
348 copy(samples[1], samples[0])
349
350
351 var wg sync.WaitGroup
352 wg.Add(1)
353 done := make(chan struct{})
354 go func() {
355 defer wg.Done()
356 for {
357
358 for i := 0; i < len(readMetricsSink); i++ {
359 readMetricsSink[i] = make([]byte, 1024)
360 select {
361 case <-done:
362 return
363 default:
364 }
365 }
366 runtime.GC()
367 }
368 }()
369
370 sum := func(us []uint64) uint64 {
371 total := uint64(0)
372 for _, u := range us {
373 total += u
374 }
375 return total
376 }
377
378
379 metrics.Read(samples[0])
380
381
382 for gen := 1; gen < 10; gen++ {
383 metrics.Read(samples[gen%2])
384 for i := range samples[gen%2] {
385 name := samples[gen%2][i].Name
386 vNew, vOld := samples[gen%2][i].Value, samples[1-(gen%2)][i].Value
387
388 switch vNew.Kind() {
389 case metrics.KindUint64:
390 new := vNew.Uint64()
391 old := vOld.Uint64()
392 if new < old {
393 t.Errorf("%s decreased: %d < %d", name, new, old)
394 }
395 case metrics.KindFloat64:
396 new := vNew.Float64()
397 old := vOld.Float64()
398 if new < old {
399 t.Errorf("%s decreased: %f < %f", name, new, old)
400 }
401 case metrics.KindFloat64Histogram:
402 new := sum(vNew.Float64Histogram().Counts)
403 old := sum(vOld.Float64Histogram().Counts)
404 if new < old {
405 t.Errorf("%s counts decreased: %d < %d", name, new, old)
406 }
407 }
408 }
409 }
410 close(done)
411
412 wg.Wait()
413 }
414
View as plain text