Source file src/runtime/metrics_test.go

     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