Skip to content

Commit 36c5edd

Browse files
committed
runtime: add timeHistogram type
This change adds a concurrent HDR time histogram to the runtime with tests. It also adds a function to generate boundaries for use by the metrics package. For #37112. Change-Id: Ifbef8ddce8e3a965a0dcd58ccd4915c282ae2098 Reviewed-on: https://go-review.googlesource.com/c/go/+/247046 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Michael Knyszek <[email protected]> Reviewed-by: Michael Pratt <[email protected]>
1 parent 8e2370b commit 36c5edd

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

src/runtime/export_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -1141,3 +1141,27 @@ func MSpanCountAlloc(ms *MSpan, bits []byte) int {
11411141
s.gcmarkBits = nil
11421142
return result
11431143
}
1144+
1145+
const (
1146+
TimeHistSubBucketBits = timeHistSubBucketBits
1147+
TimeHistNumSubBuckets = timeHistNumSubBuckets
1148+
TimeHistNumSuperBuckets = timeHistNumSuperBuckets
1149+
)
1150+
1151+
type TimeHistogram timeHistogram
1152+
1153+
// Counts returns the counts for the given bucket, subBucket indices.
1154+
// Returns true if the bucket was valid, otherwise returns the counts
1155+
// for the overflow bucket and false.
1156+
func (th *TimeHistogram) Count(bucket, subBucket uint) (uint64, bool) {
1157+
t := (*timeHistogram)(th)
1158+
i := bucket*TimeHistNumSubBuckets + subBucket
1159+
if i >= uint(len(t.counts)) {
1160+
return t.overflow, false
1161+
}
1162+
return t.counts[i], true
1163+
}
1164+
1165+
func (th *TimeHistogram) Record(duration int64) {
1166+
(*timeHistogram)(th).record(duration)
1167+
}

src/runtime/histogram.go

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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
6+
7+
import (
8+
"runtime/internal/atomic"
9+
"runtime/internal/sys"
10+
)
11+
12+
const (
13+
// For the time histogram type, we use an HDR histogram.
14+
// Values are placed in super-buckets based solely on the most
15+
// significant set bit. Thus, super-buckets are power-of-2 sized.
16+
// Values are then placed into sub-buckets based on the value of
17+
// the next timeHistSubBucketBits most significant bits. Thus,
18+
// sub-buckets are linear within a super-bucket.
19+
//
20+
// Therefore, the number of sub-buckets (timeHistNumSubBuckets)
21+
// defines the error. This error may be computed as
22+
// 1/timeHistNumSubBuckets*100%. For example, for 16 sub-buckets
23+
// per super-bucket the error is approximately 6%.
24+
//
25+
// The number of super-buckets (timeHistNumSuperBuckets), on the
26+
// other hand, defines the range. To reserve room for sub-buckets,
27+
// bit timeHistSubBucketBits is the first bit considered for
28+
// super-buckets, so super-bucket indicies are adjusted accordingly.
29+
//
30+
// As an example, consider 45 super-buckets with 16 sub-buckets.
31+
//
32+
// 00110
33+
// ^----
34+
// │ ^
35+
// │ └---- Lowest 4 bits -> sub-bucket 6
36+
// └------- Bit 4 unset -> super-bucket 0
37+
//
38+
// 10110
39+
// ^----
40+
// │ ^
41+
// │ └---- Next 4 bits -> sub-bucket 6
42+
// └------- Bit 4 set -> super-bucket 1
43+
// 100010
44+
// ^----^
45+
// │ ^ └-- Lower bits ignored
46+
// │ └---- Next 4 bits -> sub-bucket 1
47+
// └------- Bit 5 set -> super-bucket 2
48+
//
49+
// Following this pattern, bucket 45 will have the bit 48 set. We don't
50+
// have any buckets for higher values, so the highest sub-bucket will
51+
// contain values of 2^48-1 nanoseconds or approx. 3 days. This range is
52+
// more than enough to handle durations produced by the runtime.
53+
timeHistSubBucketBits = 4
54+
timeHistNumSubBuckets = 1 << timeHistSubBucketBits
55+
timeHistNumSuperBuckets = 45
56+
timeHistTotalBuckets = timeHistNumSuperBuckets*timeHistNumSubBuckets + 1
57+
)
58+
59+
// timeHistogram represents a distribution of durations in
60+
// nanoseconds.
61+
//
62+
// The accuracy and range of the histogram is defined by the
63+
// timeHistSubBucketBits and timeHistNumSuperBuckets constants.
64+
//
65+
// It is an HDR histogram with exponentially-distributed
66+
// buckets and linearly distributed sub-buckets.
67+
//
68+
// Counts in the histogram are updated atomically, so it is safe
69+
// for concurrent use. It is also safe to read all the values
70+
// atomically.
71+
type timeHistogram struct {
72+
counts [timeHistNumSuperBuckets * timeHistNumSubBuckets]uint64
73+
overflow uint64
74+
}
75+
76+
// record adds the given duration to the distribution.
77+
//
78+
// Although the duration is an int64 to facilitate ease-of-use
79+
// with e.g. nanotime, the duration must be non-negative.
80+
func (h *timeHistogram) record(duration int64) {
81+
if duration < 0 {
82+
throw("timeHistogram encountered negative duration")
83+
}
84+
// The index of the exponential bucket is just the index
85+
// of the highest set bit adjusted for how many bits we
86+
// use for the subbucket. Note that it's timeHistSubBucketsBits-1
87+
// because we use the 0th bucket to hold values < timeHistNumSubBuckets.
88+
var superBucket, subBucket uint
89+
if duration >= timeHistNumSubBuckets {
90+
// At this point, we know the duration value will always be
91+
// at least timeHistSubBucketsBits long.
92+
superBucket = uint(sys.Len64(uint64(duration))) - timeHistSubBucketBits
93+
if superBucket*timeHistNumSubBuckets >= uint(len(h.counts)) {
94+
// The bucket index we got is larger than what we support, so
95+
// add into the special overflow bucket.
96+
atomic.Xadd64(&h.overflow, 1)
97+
return
98+
}
99+
// The linear subbucket index is just the timeHistSubBucketsBits
100+
// bits after the top bit. To extract that value, shift down
101+
// the duration such that we leave the top bit and the next bits
102+
// intact, then extract the index.
103+
subBucket = uint((duration >> (superBucket - 1)) % timeHistNumSubBuckets)
104+
} else {
105+
subBucket = uint(duration)
106+
}
107+
atomic.Xadd64(&h.counts[superBucket*timeHistNumSubBuckets+subBucket], 1)
108+
}
109+
110+
// timeHistogramMetricsBuckets generates a slice of boundaries for
111+
// the timeHistogram. These boundaries are represented in seconds,
112+
// not nanoseconds like the timeHistogram represents durations.
113+
func timeHistogramMetricsBuckets() []float64 {
114+
b := make([]float64, timeHistTotalBuckets-1)
115+
for i := 0; i < timeHistNumSuperBuckets; i++ {
116+
superBucketMin := uint64(0)
117+
// The (inclusive) minimum for the first bucket is 0.
118+
if i > 0 {
119+
// The minimum for the second bucket will be
120+
// 1 << timeHistSubBucketBits, indicating that all
121+
// sub-buckets are represented by the next timeHistSubBucketBits
122+
// bits.
123+
// Thereafter, we shift up by 1 each time, so we can represent
124+
// this pattern as (i-1)+timeHistSubBucketBits.
125+
superBucketMin = uint64(1) << uint(i-1+timeHistSubBucketBits)
126+
}
127+
// subBucketShift is the amount that we need to shift the sub-bucket
128+
// index to combine it with the bucketMin.
129+
subBucketShift := uint(0)
130+
if i > 1 {
131+
// The first two buckets are exact with respect to integers,
132+
// so we'll never have to shift the sub-bucket index. Thereafter,
133+
// we shift up by 1 with each subsequent bucket.
134+
subBucketShift = uint(i - 2)
135+
}
136+
for j := 0; j < timeHistNumSubBuckets; j++ {
137+
// j is the sub-bucket index. By shifting the index into position to
138+
// combine with the bucket minimum, we obtain the minimum value for that
139+
// sub-bucket.
140+
subBucketMin := superBucketMin + (uint64(j) << subBucketShift)
141+
142+
// Convert the subBucketMin which is in nanoseconds to a float64 seconds value.
143+
// These values will all be exactly representable by a float64.
144+
b[i*timeHistNumSubBuckets+j] = float64(subBucketMin) / 1e9
145+
}
146+
}
147+
return b
148+
}

src/runtime/histogram_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
"testing"
10+
)
11+
12+
var dummyTimeHistogram TimeHistogram
13+
14+
func TestTimeHistogram(t *testing.T) {
15+
// We need to use a global dummy because this
16+
// could get stack-allocated with a non-8-byte alignment.
17+
// The result of this bad alignment is a segfault on
18+
// 32-bit platforms when calling Record.
19+
h := &dummyTimeHistogram
20+
21+
// Record exactly one sample in each bucket.
22+
for i := 0; i < TimeHistNumSuperBuckets; i++ {
23+
var base int64
24+
if i > 0 {
25+
base = int64(1) << (i + TimeHistSubBucketBits - 1)
26+
}
27+
for j := 0; j < TimeHistNumSubBuckets; j++ {
28+
v := int64(j)
29+
if i > 0 {
30+
v <<= i - 1
31+
}
32+
h.Record(base + v)
33+
}
34+
}
35+
// Hit the overflow bucket.
36+
h.Record(int64(^uint64(0) >> 1))
37+
38+
// Check to make sure there's exactly one count in each
39+
// bucket.
40+
for i := uint(0); i < TimeHistNumSuperBuckets; i++ {
41+
for j := uint(0); j < TimeHistNumSubBuckets; j++ {
42+
c, ok := h.Count(i, j)
43+
if !ok {
44+
t.Errorf("hit overflow bucket unexpectedly: (%d, %d)", i, j)
45+
} else if c != 1 {
46+
t.Errorf("bucket (%d, %d) has count that is not 1: %d", i, j, c)
47+
}
48+
}
49+
}
50+
c, ok := h.Count(TimeHistNumSuperBuckets, 0)
51+
if ok {
52+
t.Errorf("expected to hit overflow bucket: (%d, %d)", TimeHistNumSuperBuckets, 0)
53+
}
54+
if c != 1 {
55+
t.Errorf("overflow bucket has count that is not 1: %d", c)
56+
}
57+
dummyTimeHistogram = TimeHistogram{}
58+
}

src/runtime/metrics.go

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var (
2020
metrics map[string]metricData
2121

2222
sizeClassBuckets []float64
23+
timeHistBuckets []float64
2324
)
2425

2526
type metricData struct {
@@ -44,6 +45,7 @@ func initMetrics() {
4445
for i := range sizeClassBuckets {
4546
sizeClassBuckets[i] = float64(class_to_size[i])
4647
}
48+
timeHistBuckets = timeHistogramMetricsBuckets()
4749
metrics = map[string]metricData{
4850
"/gc/cycles/automatic:gc-cycles": {
4951
deps: makeStatDepSet(sysStatsDep),

0 commit comments

Comments
 (0)