]> Cypherpunks repositories - gostls13.git/commitdiff
runtime,runtime/metrics: use explicit histogram boundaries
authorMichael Anthony Knyszek <mknyszek@google.com>
Wed, 6 Jan 2021 23:05:22 +0000 (23:05 +0000)
committerMichael Knyszek <mknyszek@google.com>
Fri, 8 Jan 2021 03:43:44 +0000 (03:43 +0000)
This change modifies the semantics of
runtime/metrics.Float64Histogram.Buckets to remove implicit buckets to
that extend to positive and negative infinity and instead defines all
bucket boundaries as explicitly listed.

Bucket boundaries remain the same as before except
/gc/heap/allocs-by-size:objects and /gc/heap/frees-by-size:objects no
longer have a bucket that extends to negative infinity.

This change simplifies the Float64Histogram API, making it both easier
to understand and easier to use.

Also, add a test for allocs-by-size and frees-by-size that checks them
against MemStats.

Fixes #43443.

Change-Id: I5620f15bd084562dadf288f733c4a8cace21910c
Reviewed-on: https://go-review.googlesource.com/c/go/+/281238
Run-TryBot: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Trust: Michael Knyszek <mknyszek@google.com>

src/runtime/histogram.go
src/runtime/metrics.go
src/runtime/metrics/histogram.go
src/runtime/metrics_test.go

index d48e856cd0d8531f63b39b4607de553e4551ece9..42baa6c5e2c0df3765e21e307bfdc4c1c808a92d 100644 (file)
@@ -7,6 +7,7 @@ package runtime
 import (
        "runtime/internal/atomic"
        "runtime/internal/sys"
+       "unsafe"
 )
 
 const (
@@ -69,7 +70,13 @@ const (
 // for concurrent use. It is also safe to read all the values
 // atomically.
 type timeHistogram struct {
-       counts    [timeHistNumSuperBuckets * timeHistNumSubBuckets]uint64
+       counts [timeHistNumSuperBuckets * timeHistNumSubBuckets]uint64
+
+       // underflow counts all the times we got a negative duration
+       // sample. Because of how time works on some platforms, it's
+       // possible to measure negative durations. We could ignore them,
+       // but we record them anyway because it's better to have some
+       // signal that it's happening than just missing samples.
        underflow uint64
 }
 
@@ -107,14 +114,30 @@ func (h *timeHistogram) record(duration int64) {
        atomic.Xadd64(&h.counts[superBucket*timeHistNumSubBuckets+subBucket], 1)
 }
 
+const (
+       fInf    = 0x7FF0000000000000
+       fNegInf = 0xFFF0000000000000
+)
+
+func float64Inf() float64 {
+       inf := uint64(fInf)
+       return *(*float64)(unsafe.Pointer(&inf))
+}
+
+func float64NegInf() float64 {
+       inf := uint64(fNegInf)
+       return *(*float64)(unsafe.Pointer(&inf))
+}
+
 // timeHistogramMetricsBuckets generates a slice of boundaries for
 // the timeHistogram. These boundaries are represented in seconds,
 // not nanoseconds like the timeHistogram represents durations.
 func timeHistogramMetricsBuckets() []float64 {
-       b := make([]float64, timeHistTotalBuckets-1)
+       b := make([]float64, timeHistTotalBuckets+1)
+       b[0] = float64NegInf()
        for i := 0; i < timeHistNumSuperBuckets; i++ {
                superBucketMin := uint64(0)
-               // The (inclusive) minimum for the first bucket is 0.
+               // The (inclusive) minimum for the first non-negative bucket is 0.
                if i > 0 {
                        // The minimum for the second bucket will be
                        // 1 << timeHistSubBucketBits, indicating that all
@@ -141,8 +164,9 @@ func timeHistogramMetricsBuckets() []float64 {
 
                        // Convert the subBucketMin which is in nanoseconds to a float64 seconds value.
                        // These values will all be exactly representable by a float64.
-                       b[i*timeHistNumSubBuckets+j] = float64(subBucketMin) / 1e9
+                       b[i*timeHistNumSubBuckets+j+1] = float64(subBucketMin) / 1e9
                }
        }
+       b[len(b)-1] = float64Inf()
        return b
 }
index 1d191e62985d5e2a3d10c9bd1302a7d17465ffe5..4d37a56f4c9c9b1212c7bcf4c841bf5e9e1b5262 100644 (file)
@@ -41,8 +41,13 @@ func initMetrics() {
        if metricsInit {
                return
        }
-       sizeClassBuckets = make([]float64, _NumSizeClasses)
-       for i := range sizeClassBuckets {
+
+       sizeClassBuckets = make([]float64, _NumSizeClasses, _NumSizeClasses+1)
+       // Skip size class 0 which is a stand-in for large objects, but large
+       // objects are tracked separately (and they actually get placed in
+       // the last bucket, not the first).
+       sizeClassBuckets[0] = 1 // The smallest allocation is 1 byte in size.
+       for i := 1; i < _NumSizeClasses; i++ {
                // Size classes have an inclusive upper-bound
                // and exclusive lower bound (e.g. 48-byte size class is
                // (32, 48]) whereas we want and inclusive lower-bound
@@ -56,6 +61,8 @@ func initMetrics() {
                // boundaries.
                sizeClassBuckets[i] = float64(class_to_size[i] + 1)
        }
+       sizeClassBuckets = append(sizeClassBuckets, float64Inf())
+
        timeHistBuckets = timeHistogramMetricsBuckets()
        metrics = map[string]metricData{
                "/gc/cycles/automatic:gc-cycles": {
@@ -84,8 +91,10 @@ func initMetrics() {
                        compute: func(in *statAggregate, out *metricValue) {
                                hist := out.float64HistOrInit(sizeClassBuckets)
                                hist.counts[len(hist.counts)-1] = uint64(in.heapStats.largeAllocCount)
-                               for i := range hist.buckets {
-                                       hist.counts[i] = uint64(in.heapStats.smallAllocCount[i])
+                               // Cut off the first index which is ostensibly for size class 0,
+                               // but large objects are tracked separately so it's actually unused.
+                               for i, count := range in.heapStats.smallAllocCount[1:] {
+                                       hist.counts[i] = uint64(count)
                                }
                        },
                },
@@ -94,8 +103,10 @@ func initMetrics() {
                        compute: func(in *statAggregate, out *metricValue) {
                                hist := out.float64HistOrInit(sizeClassBuckets)
                                hist.counts[len(hist.counts)-1] = uint64(in.heapStats.largeFreeCount)
-                               for i := range hist.buckets {
-                                       hist.counts[i] = uint64(in.heapStats.smallFreeCount[i])
+                               // Cut off the first index which is ostensibly for size class 0,
+                               // but large objects are tracked separately so it's actually unused.
+                               for i, count := range in.heapStats.smallFreeCount[1:] {
+                                       hist.counts[i] = uint64(count)
                                }
                        },
                },
@@ -116,8 +127,11 @@ func initMetrics() {
                "/gc/pauses:seconds": {
                        compute: func(_ *statAggregate, out *metricValue) {
                                hist := out.float64HistOrInit(timeHistBuckets)
+                               // The bottom-most bucket, containing negative values, is tracked
+                               // as a separately as underflow, so fill that in manually and then
+                               // iterate over the rest.
                                hist.counts[0] = atomic.Load64(&memstats.gcPauseDist.underflow)
-                               for i := range hist.buckets {
+                               for i := range memstats.gcPauseDist.counts {
                                        hist.counts[i+1] = atomic.Load64(&memstats.gcPauseDist.counts[i])
                                }
                        },
@@ -437,8 +451,8 @@ func (v *metricValue) float64HistOrInit(buckets []float64) *metricFloat64Histogr
                v.pointer = unsafe.Pointer(hist)
        }
        hist.buckets = buckets
-       if len(hist.counts) != len(hist.buckets)+1 {
-               hist.counts = make([]uint64, len(buckets)+1)
+       if len(hist.counts) != len(hist.buckets)-1 {
+               hist.counts = make([]uint64, len(buckets)-1)
        }
        return hist
 }
index e1364e1e26577e9ca80474b4607e4787a5b36a39..956422bf84ea250d6c3c5c533d6be5b3990685c5 100644 (file)
@@ -6,25 +6,28 @@ package metrics
 
 // Float64Histogram represents a distribution of float64 values.
 type Float64Histogram struct {
-       // Counts contains the weights for each histogram bucket. The length of
-       // Counts is equal to the length of Buckets (in the metric description)
-       // plus one to account for the implicit minimum bucket.
+       // Counts contains the weights for each histogram bucket.
        //
-       // Given N buckets, the following is the mathematical relationship between
-       // Counts and Buckets.
-       // count[0] is the weight of the range (-inf, bucket[0])
-       // count[n] is the weight of the range [bucket[n], bucket[n+1]), for 0 < n < N-1
-       // count[N-1] is the weight of the range [bucket[N-1], inf)
+       // Given N buckets, Count[n] is the weight of the range
+       // [bucket[n], bucket[n+1]), for 0 <= n < N.
        Counts []uint64
 
-       // Buckets contains the boundaries between histogram buckets, in increasing order.
+       // Buckets contains the boundaries of the histogram buckets, in increasing order.
        //
-       // Because this slice contains boundaries, there are len(Buckets)+1 counts:
-       // a count for all values less than the first boundary, a count covering each
-       // [slice[i], slice[i+1]) interval, and a count for all values greater than or
-       // equal to the last boundary.
+       // Buckets[0] is the inclusive lower bound of the minimum bucket while
+       // Buckets[len(Buckets)-1] is the exclusive upper bound of the maximum bucket.
+       // Hence, there are len(Buckets)-1 counts. Furthermore, len(Buckets) != 1, always,
+       // since at least two boundaries are required to describe one bucket (and 0
+       // boundaries are used to describe 0 buckets).
+       //
+       // Buckets[0] is permitted to have value -Inf and Buckets[len(Buckets)-1] is
+       // permitted to have value Inf.
        //
        // For a given metric name, the value of Buckets is guaranteed not to change
        // between calls until program exit.
+       //
+       // This slice value is permitted to alias with other Float64Histograms' Buckets
+       // fields, so the values within should only ever be read. If they need to be
+       // modified, the user must make a copy.
        Buckets []float64
 }
index 0ee469ae29406c38b32aaf6a371c8f2fd9e436d8..5109058ed167f1a36881072f6aad6887865dea15 100644 (file)
@@ -70,6 +70,34 @@ func TestReadMetrics(t *testing.T) {
                        checkUint64(t, name, samples[i].Value.Uint64(), mstats.BuckHashSys)
                case "/memory/classes/total:bytes":
                        checkUint64(t, name, samples[i].Value.Uint64(), mstats.Sys)
+               case "/gc/heap/allocs-by-size:objects":
+                       hist := samples[i].Value.Float64Histogram()
+                       // Skip size class 0 in BySize, because it's always empty and not represented
+                       // in the histogram.
+                       for i, sc := range mstats.BySize[1:] {
+                               if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
+                                       t.Errorf("bucket does not match size class: got %f, want %f", b, s)
+                                       // The rest of the checks aren't expected to work anyway.
+                                       continue
+                               }
+                               if c, m := hist.Counts[i], sc.Mallocs; c != m {
+                                       t.Errorf("histogram counts do not much BySize for class %d: got %d, want %d", i, c, m)
+                               }
+                       }
+               case "/gc/heap/frees-by-size:objects":
+                       hist := samples[i].Value.Float64Histogram()
+                       // Skip size class 0 in BySize, because it's always empty and not represented
+                       // in the histogram.
+                       for i, sc := range mstats.BySize[1:] {
+                               if b, s := hist.Buckets[i+1], float64(sc.Size+1); b != s {
+                                       t.Errorf("bucket does not match size class: got %f, want %f", b, s)
+                                       // The rest of the checks aren't expected to work anyway.
+                                       continue
+                               }
+                               if c, f := hist.Counts[i], sc.Frees; c != f {
+                                       t.Errorf("histogram counts do not much BySize for class %d: got %d, want %d", i, c, f)
+                               }
+                       }
                case "/gc/heap/objects:objects":
                        checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapObjects)
                case "/gc/heap/goal:bytes":
@@ -154,11 +182,11 @@ func TestReadMetricsConsistency(t *testing.T) {
        if totalVirtual.got != totalVirtual.want {
                t.Errorf(`"/memory/classes/total:bytes" does not match sum of /memory/classes/**: got %d, want %d`, totalVirtual.got, totalVirtual.want)
        }
-       if objects.alloc.Counts[0] > 0 {
-               t.Error("found counts for objects of non-positive size in allocs-by-size")
+       if b, c := len(objects.alloc.Buckets), len(objects.alloc.Counts); b != c+1 {
+               t.Errorf("allocs-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
        }
-       if objects.free.Counts[0] > 0 {
-               t.Error("found counts for objects of non-positive size in frees-by-size")
+       if b, c := len(objects.free.Buckets), len(objects.free.Counts); b != c+1 {
+               t.Errorf("frees-by-size has wrong bucket or counts length: %d buckets, %d counts", b, c)
        }
        if len(objects.alloc.Buckets) != len(objects.free.Buckets) {
                t.Error("allocs-by-size and frees-by-size buckets don't match in length")