github.com/m10x/go/src@v0.0.0-20220112094212-ba61592315da/runtime/metrics_test.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 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 // Tests whether readMetrics produces values aligning 30 // with ReadMemStats while the world is stopped. 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 // Check to make sure the values we read line up with other values we read. 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 // Skip size class 0 in BySize, because it's always empty and not represented 79 // in the histogram. 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 // The rest of the checks aren't expected to work anyway. 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 // Skip size class 0 in BySize, because it's always empty and not represented 96 // in the histogram. 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 // The rest of the checks aren't expected to work anyway. 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 // Currently, MemStats adds tiny alloc count to both Mallocs AND Frees. 111 // The reason for this is because MemStats couldn't be extended at the time 112 // but there was a desire to have Mallocs at least be a little more representative, 113 // while having Mallocs - Frees still represent a live object count. 114 // Unfortunately, MemStats doesn't actually export a large allocation count, 115 // so it's impossible to pull this number out directly. 116 // 117 // Check tiny allocation count outside of this loop, by using the allocs-by-size 118 // histogram in order to figure out how many large objects there are. 119 tinyAllocs = samples[i].Value.Uint64() 120 // Because the next two metrics tests are checking against Mallocs and Frees, 121 // we can't check them directly for the same reason: we need to account for tiny 122 // allocations included in Mallocs and Frees. 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 // Check tinyAllocs. 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 // Check allocation and free counts. 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 // Tests whether readMetrics produces consistent, sensible values. 154 // The values are read concurrently with the runtime doing other 155 // things (e.g. allocating) so what we read can't reasonably compared 156 // to runtime values. 157 158 // Run a few GC cycles to get some of the stats to be non-zero. 159 runtime.GC() 160 runtime.GC() 161 runtime.GC() 162 163 // Read all the supported metrics through the metrics package. 164 descs, samples := prepareAllMetricsSamples() 165 metrics.Read(samples) 166 167 // Check to make sure the values we read make sense. 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 // None of these stats should ever get this big. 192 // If they do, there's probably overflow involved, 193 // usually due to bad accounting. 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 // The current GC has at least 2 pauses per GC. 281 // Check to see if that value makes sense. 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 // Spend this much time measuring latencies. 291 latencies := make([]time.Duration, 0, 1024) 292 _, samples := prepareAllMetricsSamples() 293 294 // Hit metrics.Read continuously and measure. 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 // Make sure to stop the timer before we wait! The load created above 302 // is very heavy-weight and not easy to stop, so we could end up 303 // confusing the benchmarking framework for small b.N. 304 b.StopTimer() 305 stop() 306 307 // Disable the default */op metrics. 308 // ns/op doesn't mean anything because it's an average, but we 309 // have a sleep in our b.N loop above which skews this significantly. 310 b.ReportMetric(0, "ns/op") 311 b.ReportMetric(0, "B/op") 312 b.ReportMetric(0, "allocs/op") 313 314 // Sort latencies then report percentiles. 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 }