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  	"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  	// Tests whether readMetrics produces values aligning
    31  	// with ReadMemStats while the world is stopped.
    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  	// Check to make sure the values we read line up with other values we read.
    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  			// Skip size class 0 in BySize, because it's always empty and not represented
    82  			// in the histogram.
    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  					// The rest of the checks aren't expected to work anyway.
    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  			// Skip size class 0 in BySize, because it's always empty and not represented
    99  			// in the histogram.
   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  					// The rest of the checks aren't expected to work anyway.
   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  			// Currently, MemStats adds tiny alloc count to both Mallocs AND Frees.
   114  			// The reason for this is because MemStats couldn't be extended at the time
   115  			// but there was a desire to have Mallocs at least be a little more representative,
   116  			// while having Mallocs - Frees still represent a live object count.
   117  			// Unfortunately, MemStats doesn't actually export a large allocation count,
   118  			// so it's impossible to pull this number out directly.
   119  			//
   120  			// Check tiny allocation count outside of this loop, by using the allocs-by-size
   121  			// histogram in order to figure out how many large objects there are.
   122  			tinyAllocs = samples[i].Value.Uint64()
   123  			// Because the next two metrics tests are checking against Mallocs and Frees,
   124  			// we can't check them directly for the same reason: we need to account for tiny
   125  			// allocations included in Mallocs and Frees.
   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  	// Check tinyAllocs.
   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  	// Check allocation and free counts.
   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  	// Tests whether readMetrics produces consistent, sensible values.
   157  	// The values are read concurrently with the runtime doing other
   158  	// things (e.g. allocating) so what we read can't reasonably compared
   159  	// to runtime values.
   160  
   161  	// Run a few GC cycles to get some of the stats to be non-zero.
   162  	runtime.GC()
   163  	runtime.GC()
   164  	runtime.GC()
   165  
   166  	// Read all the supported metrics through the metrics package.
   167  	descs, samples := prepareAllMetricsSamples()
   168  	metrics.Read(samples)
   169  
   170  	// Check to make sure the values we read make sense.
   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  			// None of these stats should ever get this big.
   195  			// If they do, there's probably overflow involved,
   196  			// usually due to bad accounting.
   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  	// The current GC has at least 2 pauses per GC.
   288  	// Check to see if that value makes sense.
   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  	// Spend this much time measuring latencies.
   298  	latencies := make([]time.Duration, 0, 1024)
   299  	_, samples := prepareAllMetricsSamples()
   300  
   301  	// Hit metrics.Read continuously and measure.
   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  	// Make sure to stop the timer before we wait! The load created above
   309  	// is very heavy-weight and not easy to stop, so we could end up
   310  	// confusing the benchmarking framework for small b.N.
   311  	b.StopTimer()
   312  	stop()
   313  
   314  	// Disable the default */op metrics.
   315  	// ns/op doesn't mean anything because it's an average, but we
   316  	// have a sleep in our b.N loop above which skews this significantly.
   317  	b.ReportMetric(0, "ns/op")
   318  	b.ReportMetric(0, "B/op")
   319  	b.ReportMetric(0, "allocs/op")
   320  
   321  	// Sort latencies then report percentiles.
   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  	// Set up the set of metrics marked cumulative.
   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  	// Start some noise in the background.
   351  	var wg sync.WaitGroup
   352  	wg.Add(1)
   353  	done := make(chan struct{})
   354  	go func() {
   355  		defer wg.Done()
   356  		for {
   357  			// Add more things here that could influence metrics.
   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  	// Populate the first generation.
   379  	metrics.Read(samples[0])
   380  
   381  	// Check to make sure that these metrics only grow monotonically.
   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