Skip to content

Commit a4d9977

Browse files
mknyszekgopherbot
authored andcommitted
runtime/metrics: add cleanup and finalizer queue metrics
These metrics are useful for identifying finalizer and cleanup problems, namely slow finalizers and/or cleanups holding up the queue, which can lead to a memory leak. Fixes #72948. Change-Id: I1bb64a9ca751fcb462c96d986d0346e0c2894c95 Reviewed-on: https://go-review.googlesource.com/c/go/+/690396 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Carlos Amedee <[email protected]> Auto-Submit: Michael Knyszek <[email protected]>
1 parent 70a2ff7 commit a4d9977

File tree

4 files changed

+179
-9
lines changed

4 files changed

+179
-9
lines changed

src/runtime/metrics.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ func initMetrics() {
169169
out.scalar = float64bits(nsToSec(in.cpuStats.UserTime))
170170
},
171171
},
172+
"/gc/cleanups/executed:cleanups": {
173+
deps: makeStatDepSet(finalStatsDep),
174+
compute: func(in *statAggregate, out *metricValue) {
175+
out.kind = metricKindUint64
176+
out.scalar = in.finalStats.cleanupsExecuted
177+
},
178+
},
179+
"/gc/cleanups/queued:cleanups": {
180+
deps: makeStatDepSet(finalStatsDep),
181+
compute: func(in *statAggregate, out *metricValue) {
182+
out.kind = metricKindUint64
183+
out.scalar = in.finalStats.cleanupsQueued
184+
},
185+
},
172186
"/gc/cycles/automatic:gc-cycles": {
173187
deps: makeStatDepSet(sysStatsDep),
174188
compute: func(in *statAggregate, out *metricValue) {
@@ -190,6 +204,20 @@ func initMetrics() {
190204
out.scalar = in.sysStats.gcCyclesDone
191205
},
192206
},
207+
"/gc/finalizers/executed:finalizers": {
208+
deps: makeStatDepSet(finalStatsDep),
209+
compute: func(in *statAggregate, out *metricValue) {
210+
out.kind = metricKindUint64
211+
out.scalar = in.finalStats.finalizersExecuted
212+
},
213+
},
214+
"/gc/finalizers/queued:finalizers": {
215+
deps: makeStatDepSet(finalStatsDep),
216+
compute: func(in *statAggregate, out *metricValue) {
217+
out.kind = metricKindUint64
218+
out.scalar = in.finalStats.finalizersQueued
219+
},
220+
},
193221
"/gc/scan/globals:bytes": {
194222
deps: makeStatDepSet(gcStatsDep),
195223
compute: func(in *statAggregate, out *metricValue) {
@@ -514,10 +542,11 @@ func godebug_registerMetric(name string, read func() uint64) {
514542
type statDep uint
515543

516544
const (
517-
heapStatsDep statDep = iota // corresponds to heapStatsAggregate
518-
sysStatsDep // corresponds to sysStatsAggregate
519-
cpuStatsDep // corresponds to cpuStatsAggregate
520-
gcStatsDep // corresponds to gcStatsAggregate
545+
heapStatsDep statDep = iota // corresponds to heapStatsAggregate
546+
sysStatsDep // corresponds to sysStatsAggregate
547+
cpuStatsDep // corresponds to cpuStatsAggregate
548+
gcStatsDep // corresponds to gcStatsAggregate
549+
finalStatsDep // corresponds to finalStatsAggregate
521550
numStatsDeps
522551
)
523552

@@ -696,6 +725,21 @@ func (a *gcStatsAggregate) compute() {
696725
a.totalScan = a.heapScan + a.stackScan + a.globalsScan
697726
}
698727

728+
// finalStatsAggregate represents various finalizer/cleanup stats obtained
729+
// from the runtime acquired together to avoid skew and inconsistencies.
730+
type finalStatsAggregate struct {
731+
finalizersQueued uint64
732+
finalizersExecuted uint64
733+
cleanupsQueued uint64
734+
cleanupsExecuted uint64
735+
}
736+
737+
// compute populates the finalStatsAggregate with values from the runtime.
738+
func (a *finalStatsAggregate) compute() {
739+
a.finalizersQueued, a.finalizersExecuted = finReadQueueStats()
740+
a.cleanupsQueued, a.cleanupsExecuted = gcCleanups.readQueueStats()
741+
}
742+
699743
// nsToSec takes a duration in nanoseconds and converts it to seconds as
700744
// a float64.
701745
func nsToSec(ns int64) float64 {
@@ -708,11 +752,12 @@ func nsToSec(ns int64) float64 {
708752
// as a set of these aggregates that it has populated. The aggregates
709753
// are populated lazily by its ensure method.
710754
type statAggregate struct {
711-
ensured statDepSet
712-
heapStats heapStatsAggregate
713-
sysStats sysStatsAggregate
714-
cpuStats cpuStatsAggregate
715-
gcStats gcStatsAggregate
755+
ensured statDepSet
756+
heapStats heapStatsAggregate
757+
sysStats sysStatsAggregate
758+
cpuStats cpuStatsAggregate
759+
gcStats gcStatsAggregate
760+
finalStats finalStatsAggregate
716761
}
717762

718763
// ensure populates statistics aggregates determined by deps if they
@@ -735,6 +780,8 @@ func (a *statAggregate) ensure(deps *statDepSet) {
735780
a.cpuStats.compute()
736781
case gcStatsDep:
737782
a.gcStats.compute()
783+
case finalStatsDep:
784+
a.finalStats.compute()
738785
}
739786
}
740787
a.ensured = a.ensured.union(missing)

src/runtime/metrics/description.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,22 @@ var allDesc = []Description{
174174
Kind: KindFloat64,
175175
Cumulative: true,
176176
},
177+
{
178+
Name: "/gc/cleanups/executed:cleanups",
179+
Description: "Approximate total count of cleanup functions (created by runtime.AddCleanup) " +
180+
"executed by the runtime. Subtract /gc/cleanups/queued:cleanups to approximate " +
181+
"cleanup queue length. Useful for detecting slow cleanups holding up the queue.",
182+
Kind: KindUint64,
183+
Cumulative: true,
184+
},
185+
{
186+
Name: "/gc/cleanups/queued:cleanups",
187+
Description: "Approximate total count of cleanup functions (created by runtime.AddCleanup) " +
188+
"queued by the runtime for execution. Subtract from /gc/cleanups/executed:cleanups " +
189+
"to approximate cleanup queue length. Useful for detecting slow cleanups holding up the queue.",
190+
Kind: KindUint64,
191+
Cumulative: true,
192+
},
177193
{
178194
Name: "/gc/cycles/automatic:gc-cycles",
179195
Description: "Count of completed GC cycles generated by the Go runtime.",
@@ -192,6 +208,23 @@ var allDesc = []Description{
192208
Kind: KindUint64,
193209
Cumulative: true,
194210
},
211+
{
212+
Name: "/gc/finalizers/executed:finalizers",
213+
Description: "Total count of finalizer functions (created by runtime.SetFinalizer) " +
214+
"executed by the runtime. Subtract /gc/finalizers/queued:finalizers to approximate " +
215+
"finalizer queue length. Useful for detecting finalizers overwhelming the queue, " +
216+
"either by being too slow, or by there being too many of them.",
217+
Kind: KindUint64,
218+
Cumulative: true,
219+
},
220+
{
221+
Name: "/gc/finalizers/queued:finalizers",
222+
Description: "Total count of finalizer functions (created by runtime.SetFinalizer) and " +
223+
"queued by the runtime for execution. Subtract from /gc/finalizers/executed:finalizers " +
224+
"to approximate finalizer queue length. Useful for detecting slow finalizers holding up the queue.",
225+
Kind: KindUint64,
226+
Cumulative: true,
227+
},
195228
{
196229
Name: "/gc/gogc:percent",
197230
Description: "Heap size target percentage configured by the user, otherwise 100. This " +

src/runtime/metrics/doc.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ Below is the full list of supported metrics, ordered lexicographically.
137137
to system CPU time measurements. Compare only with other
138138
/cpu/classes metrics.
139139
140+
/gc/cleanups/executed:cleanups
141+
Approximate total count of cleanup functions (created
142+
by runtime.AddCleanup) executed by the runtime. Subtract
143+
/gc/cleanups/queued:cleanups to approximate cleanup queue
144+
length. Useful for detecting slow cleanups holding up the queue.
145+
146+
/gc/cleanups/queued:cleanups
147+
Approximate total count of cleanup functions (created by
148+
runtime.AddCleanup) queued by the runtime for execution.
149+
Subtract from /gc/cleanups/executed:cleanups to approximate
150+
cleanup queue length. Useful for detecting slow cleanups holding
151+
up the queue.
152+
140153
/gc/cycles/automatic:gc-cycles
141154
Count of completed GC cycles generated by the Go runtime.
142155
@@ -146,6 +159,20 @@ Below is the full list of supported metrics, ordered lexicographically.
146159
/gc/cycles/total:gc-cycles
147160
Count of all completed GC cycles.
148161
162+
/gc/finalizers/executed:finalizers
163+
Total count of finalizer functions (created by
164+
runtime.SetFinalizer) executed by the runtime. Subtract
165+
/gc/finalizers/queued:finalizers to approximate finalizer queue
166+
length. Useful for detecting finalizers overwhelming the queue,
167+
either by being too slow, or by there being too many of them.
168+
169+
/gc/finalizers/queued:finalizers
170+
Total count of finalizer functions (created by
171+
runtime.SetFinalizer) and queued by the runtime for execution.
172+
Subtract from /gc/finalizers/executed:finalizers to approximate
173+
finalizer queue length. Useful for detecting slow finalizers
174+
holding up the queue.
175+
149176
/gc/gogc:percent
150177
Heap size target percentage configured by the user, otherwise
151178
100. This value is set by the GOGC environment variable, and the

src/runtime/metrics_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@ func TestReadMetricsCumulative(t *testing.T) {
499499
defer wg.Done()
500500
for {
501501
// Add more things here that could influence metrics.
502+
for i := 0; i < 10; i++ {
503+
runtime.AddCleanup(new(*int), func(_ struct{}) {}, struct{}{})
504+
runtime.SetFinalizer(new(*int), func(_ **int) {})
505+
}
502506
for i := 0; i < len(readMetricsSink); i++ {
503507
readMetricsSink[i] = make([]byte, 1024)
504508
select {
@@ -1512,3 +1516,62 @@ func TestMetricHeapUnusedLargeObjectOverflow(t *testing.T) {
15121516
done <- struct{}{}
15131517
wg.Wait()
15141518
}
1519+
1520+
func TestReadMetricsCleanups(t *testing.T) {
1521+
runtime.GC() // End any in-progress GC.
1522+
runtime.BlockUntilEmptyCleanupQueue(int64(1 * time.Second)) // Flush any queued cleanups.
1523+
1524+
var before [2]metrics.Sample
1525+
before[0].Name = "/gc/cleanups/queued:cleanups"
1526+
before[1].Name = "/gc/cleanups/executed:cleanups"
1527+
after := before
1528+
1529+
metrics.Read(before[:])
1530+
1531+
const N = 10
1532+
for i := 0; i < N; i++ {
1533+
runtime.AddCleanup(new(*int), func(_ struct{}) {}, struct{}{})
1534+
}
1535+
1536+
runtime.GC()
1537+
runtime.BlockUntilEmptyCleanupQueue(int64(1 * time.Second))
1538+
1539+
metrics.Read(after[:])
1540+
1541+
if v0, v1 := before[0].Value.Uint64(), after[0].Value.Uint64(); v0+N != v1 {
1542+
t.Errorf("expected %s difference to be exactly %d, got %d -> %d", before[0].Name, N, v0, v1)
1543+
}
1544+
if v0, v1 := before[1].Value.Uint64(), after[1].Value.Uint64(); v0+N != v1 {
1545+
t.Errorf("expected %s difference to be exactly %d, got %d -> %d", before[1].Name, N, v0, v1)
1546+
}
1547+
}
1548+
1549+
func TestReadMetricsFinalizers(t *testing.T) {
1550+
runtime.GC() // End any in-progress GC.
1551+
runtime.BlockUntilEmptyFinalizerQueue(int64(1 * time.Second)) // Flush any queued finalizers.
1552+
1553+
var before [2]metrics.Sample
1554+
before[0].Name = "/gc/finalizers/queued:finalizers"
1555+
before[1].Name = "/gc/finalizers/executed:finalizers"
1556+
after := before
1557+
1558+
metrics.Read(before[:])
1559+
1560+
const N = 10
1561+
for i := 0; i < N; i++ {
1562+
runtime.SetFinalizer(new(*int), func(_ **int) {})
1563+
}
1564+
1565+
runtime.GC()
1566+
runtime.GC()
1567+
runtime.BlockUntilEmptyFinalizerQueue(int64(1 * time.Second))
1568+
1569+
metrics.Read(after[:])
1570+
1571+
if v0, v1 := before[0].Value.Uint64(), after[0].Value.Uint64(); v0+N != v1 {
1572+
t.Errorf("expected %s difference to be exactly %d, got %d -> %d", before[0].Name, N, v0, v1)
1573+
}
1574+
if v0, v1 := before[1].Value.Uint64(), after[1].Value.Uint64(); v0+N != v1 {
1575+
t.Errorf("expected %s difference to be exactly %d, got %d -> %d", before[1].Name, N, v0, v1)
1576+
}
1577+
}

0 commit comments

Comments
 (0)