From 338af91ae62e85f30816209ae17b601046b62858 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:26:19 -0500 Subject: [PATCH] runtime: implement MemStats.NumGC Reading runtime.MemStats.NumGC is common enough in dependency code that its absence is a compile error for programs that never look at the value. Rather than add the field as a constant zero, track it: - gc_blocks: count completed cycles in runGC, so collections triggered by an allocation are counted as well as explicit runtime.GC() calls. The counter is read and written under gcLock, like the other counters beside it. - gc_boehm: report bdwgc's own gc_no from the prof_stats struct. - gc_leaking: always 0, since that collector never completes a cycle. --- builder/sizes_test.go | 6 +++--- src/runtime/gc_blocks.go | 9 +++++++++ src/runtime/gc_boehm.go | 1 + src/runtime/gc_leaking.go | 1 + src/runtime/mstats.go | 5 +++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/builder/sizes_test.go b/builder/sizes_test.go index 639c32b5c..0117065b6 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) { // This is a small number of very diverse targets that we want to test. tests := []sizeTest{ // microcontrollers - {"hifive1b", "examples/echo", 4313, 323, 0, 2260}, - {"microbit", "examples/serial", 2838, 382, 8, 2256}, - {"wioterminal", "examples/pininterrupt", 8027, 1665, 132, 7488}, + {"hifive1b", "examples/echo", 4321, 323, 0, 2268}, + {"microbit", "examples/serial", 2842, 382, 8, 2264}, + {"wioterminal", "examples/pininterrupt", 8039, 1665, 132, 7496}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/src/runtime/gc_blocks.go b/src/runtime/gc_blocks.go index 78387cb31..3afed0a3e 100644 --- a/src/runtime/gc_blocks.go +++ b/src/runtime/gc_blocks.go @@ -56,6 +56,7 @@ var ( endBlock gcBlock // the block just past the end of the available space gcTotalAlloc uint64 // total number of bytes allocated gcMallocs uint64 // total number of allocations + gcNumGC uint32 // total number of completed collection cycles gcLock task.PMutex // lock to avoid race conditions on multicore systems ) @@ -610,6 +611,11 @@ func runGC() (freeBytes uintptr) { dumpHeap() } + // The cycle is complete. Counted here rather than in GC() so that + // collections triggered by an allocation are counted too. Every caller + // holds gcLock, the same lock ReadMemStats reads it under. + gcNumGC++ + return } @@ -850,6 +856,9 @@ func ReadMemStats(m *MemStats) { // Record the total allocated bytes. m.TotalAlloc = gcTotalAlloc + // Record the number of completed collection cycles. + m.NumGC = gcNumGC + gcLock.Unlock() } diff --git a/src/runtime/gc_boehm.go b/src/runtime/gc_boehm.go index e0fc16a67..66a78697f 100644 --- a/src/runtime/gc_boehm.go +++ b/src/runtime/gc_boehm.go @@ -129,6 +129,7 @@ func ReadMemStats(m *MemStats) { m.Mallocs = 0 // not provided by bdwgc m.Frees = 0 // not provided by bdwgc m.Sys = uint64(gcMemStats.obtained_from_os_bytes) + m.NumGC = uint32(gcMemStats.gc_no) gcLock.Unlock() } diff --git a/src/runtime/gc_leaking.go b/src/runtime/gc_leaking.go index 3f1595ff6..839acd8d9 100644 --- a/src/runtime/gc_leaking.go +++ b/src/runtime/gc_leaking.go @@ -109,6 +109,7 @@ func ReadMemStats(m *MemStats) { m.HeapAlloc = gcTotalAlloc m.HeapObjects = gcMallocs m.Alloc = m.HeapAlloc + m.NumGC = 0 // this GC never collects, so no cycle ever completes gcLock.Unlock() } diff --git a/src/runtime/mstats.go b/src/runtime/mstats.go index 987e17c30..4271bab9a 100644 --- a/src/runtime/mstats.go +++ b/src/runtime/mstats.go @@ -82,4 +82,9 @@ type MemStats struct { // GCSys is bytes of memory in garbage collection metadata. GCSys uint64 + + // NumGC is the number of completed GC cycles. + // + // The leaking collector never collects, so it always reports 0. + NumGC uint32 }