Skip to content

Commit 8e2370b

Browse files
committed
runtime,runtime/metrics: add object size distribution metrics
This change adds metrics for the distribution of objects allocated and freed by size, mirroring MemStats' BySize field. For #37112. Change-Id: Ibaf1812da93598b37265ec97abc6669c1a5efcbf Reviewed-on: https://go-review.googlesource.com/c/go/+/247045 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 c305e49 commit 8e2370b

File tree

4 files changed

+104
-0
lines changed

4 files changed

+104
-0
lines changed

src/runtime/metrics.go

+52
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ var (
1818
metricsSema uint32 = 1
1919
metricsInit bool
2020
metrics map[string]metricData
21+
22+
sizeClassBuckets []float64
2123
)
2224

2325
type metricData struct {
@@ -38,6 +40,10 @@ func initMetrics() {
3840
if metricsInit {
3941
return
4042
}
43+
sizeClassBuckets = make([]float64, _NumSizeClasses)
44+
for i := range sizeClassBuckets {
45+
sizeClassBuckets[i] = float64(class_to_size[i])
46+
}
4147
metrics = map[string]metricData{
4248
"/gc/cycles/automatic:gc-cycles": {
4349
deps: makeStatDepSet(sysStatsDep),
@@ -60,6 +66,26 @@ func initMetrics() {
6066
out.scalar = in.sysStats.gcCyclesDone
6167
},
6268
},
69+
"/gc/heap/allocs-by-size:objects": {
70+
deps: makeStatDepSet(heapStatsDep),
71+
compute: func(in *statAggregate, out *metricValue) {
72+
hist := out.float64HistOrInit(sizeClassBuckets)
73+
hist.counts[len(hist.counts)-1] = uint64(in.heapStats.largeAllocCount)
74+
for i := range hist.buckets {
75+
hist.counts[i] = uint64(in.heapStats.smallAllocCount[i])
76+
}
77+
},
78+
},
79+
"/gc/heap/frees-by-size:objects": {
80+
deps: makeStatDepSet(heapStatsDep),
81+
compute: func(in *statAggregate, out *metricValue) {
82+
hist := out.float64HistOrInit(sizeClassBuckets)
83+
hist.counts[len(hist.counts)-1] = uint64(in.heapStats.largeFreeCount)
84+
for i := range hist.buckets {
85+
hist.counts[i] = uint64(in.heapStats.smallFreeCount[i])
86+
}
87+
},
88+
},
6389
"/gc/heap/goal:bytes": {
6490
deps: makeStatDepSet(sysStatsDep),
6591
compute: func(in *statAggregate, out *metricValue) {
@@ -370,6 +396,32 @@ type metricValue struct {
370396
pointer unsafe.Pointer // contains non-scalar values.
371397
}
372398

399+
// float64HistOrInit tries to pull out an existing float64Histogram
400+
// from the value, but if none exists, then it allocates one with
401+
// the given buckets.
402+
func (v *metricValue) float64HistOrInit(buckets []float64) *metricFloat64Histogram {
403+
var hist *metricFloat64Histogram
404+
if v.kind == metricKindFloat64Histogram && v.pointer != nil {
405+
hist = (*metricFloat64Histogram)(v.pointer)
406+
} else {
407+
v.kind = metricKindFloat64Histogram
408+
hist = new(metricFloat64Histogram)
409+
v.pointer = unsafe.Pointer(hist)
410+
}
411+
hist.buckets = buckets
412+
if len(hist.counts) != len(hist.buckets)+1 {
413+
hist.counts = make([]uint64, len(buckets)+1)
414+
}
415+
return hist
416+
}
417+
418+
// metricFloat64Histogram is a runtime copy of runtime/metrics.Float64Histogram
419+
// and must be kept structurally identical to that type.
420+
type metricFloat64Histogram struct {
421+
counts []uint64
422+
buckets []float64
423+
}
424+
373425
// agg is used by readMetrics, and is protected by metricsSema.
374426
//
375427
// Managed as a global variable because its pointer will be

src/runtime/metrics/description.go

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ var allDesc = []Description{
6868
Kind: KindUint64,
6969
Cumulative: true,
7070
},
71+
{
72+
Name: "/gc/heap/allocs-by-size:objects",
73+
Description: "Distribution of all objects allocated by approximate size.",
74+
Kind: KindFloat64Histogram,
75+
},
76+
{
77+
Name: "/gc/heap/frees-by-size:objects",
78+
Description: "Distribution of all objects freed by approximate size.",
79+
Kind: KindFloat64Histogram,
80+
},
7181
{
7282
Name: "/gc/heap/goal:bytes",
7383
Description: "Heap size target for the end of the GC cycle.",

src/runtime/metrics/doc.go

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ Supported metrics
5353
/gc/cycles/total:gc-cycles
5454
Count of all completed GC cycles.
5555
56+
/gc/heap/allocs-by-size:objects
57+
Distribution of all objects allocated by approximate size.
58+
59+
/gc/heap/frees-by-size:objects
60+
Distribution of all objects freed by approximate size.
61+
5662
/gc/heap/goal:bytes
5763
Heap size target for the end of the GC cycle.
5864

src/runtime/metrics_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ func TestReadMetricsConsistency(t *testing.T) {
9898
var totalVirtual struct {
9999
got, want uint64
100100
}
101+
var objects struct {
102+
alloc, free *metrics.Float64Histogram
103+
total uint64
104+
}
101105
for i := range samples {
102106
kind := samples[i].Value.Kind()
103107
if want := descs[samples[i].Name].Kind; kind != want {
@@ -118,11 +122,43 @@ func TestReadMetricsConsistency(t *testing.T) {
118122
switch samples[i].Name {
119123
case "/memory/classes/total:bytes":
120124
totalVirtual.got = samples[i].Value.Uint64()
125+
case "/gc/heap/objects:objects":
126+
objects.total = samples[i].Value.Uint64()
127+
case "/gc/heap/allocs-by-size:objects":
128+
objects.alloc = samples[i].Value.Float64Histogram()
129+
case "/gc/heap/frees-by-size:objects":
130+
objects.free = samples[i].Value.Float64Histogram()
121131
}
122132
}
123133
if totalVirtual.got != totalVirtual.want {
124134
t.Errorf(`"/memory/classes/total:bytes" does not match sum of /memory/classes/**: got %d, want %d`, totalVirtual.got, totalVirtual.want)
125135
}
136+
if len(objects.alloc.Buckets) != len(objects.free.Buckets) {
137+
t.Error("allocs-by-size and frees-by-size buckets don't match in length")
138+
} else if len(objects.alloc.Counts) != len(objects.free.Counts) {
139+
t.Error("allocs-by-size and frees-by-size counts don't match in length")
140+
} else {
141+
for i := range objects.alloc.Buckets {
142+
ba := objects.alloc.Buckets[i]
143+
bf := objects.free.Buckets[i]
144+
if ba != bf {
145+
t.Errorf("bucket %d is different for alloc and free hists: %f != %f", i, ba, bf)
146+
}
147+
}
148+
if !t.Failed() {
149+
got, want := uint64(0), objects.total
150+
for i := range objects.alloc.Counts {
151+
if objects.alloc.Counts[i] < objects.free.Counts[i] {
152+
t.Errorf("found more allocs than frees in object dist bucket %d", i)
153+
continue
154+
}
155+
got += objects.alloc.Counts[i] - objects.free.Counts[i]
156+
}
157+
if got != want {
158+
t.Errorf("object distribution counts don't match count of live objects: got %d, want %d", got, want)
159+
}
160+
}
161+
}
126162
}
127163

128164
func BenchmarkReadMetricsLatency(b *testing.B) {

0 commit comments

Comments
 (0)