...

Source file src/runtime/metrics_test.go

Documentation: runtime

		 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  }
	 322  

View as plain text