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.
This commit is contained in:
Moses Narrow
2026-08-13 20:26:19 -05:00
committed by Damian Gryski
parent d74f70bdd0
commit 338af91ae6
5 changed files with 19 additions and 3 deletions
+3 -3
View File
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
// This is a small number of very diverse targets that we want to test. // This is a small number of very diverse targets that we want to test.
tests := []sizeTest{ tests := []sizeTest{
// microcontrollers // microcontrollers
{"hifive1b", "examples/echo", 4313, 323, 0, 2260}, {"hifive1b", "examples/echo", 4321, 323, 0, 2268},
{"microbit", "examples/serial", 2838, 382, 8, 2256}, {"microbit", "examples/serial", 2842, 382, 8, 2264},
{"wioterminal", "examples/pininterrupt", 8027, 1665, 132, 7488}, {"wioterminal", "examples/pininterrupt", 8039, 1665, 132, 7496},
// TODO: also check wasm. Right now this is difficult, because // TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the // wasm binaries are run through wasm-opt and therefore the
+9
View File
@@ -56,6 +56,7 @@ var (
endBlock gcBlock // the block just past the end of the available space endBlock gcBlock // the block just past the end of the available space
gcTotalAlloc uint64 // total number of bytes allocated gcTotalAlloc uint64 // total number of bytes allocated
gcMallocs uint64 // total number of allocations 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 gcLock task.PMutex // lock to avoid race conditions on multicore systems
) )
@@ -610,6 +611,11 @@ func runGC() (freeBytes uintptr) {
dumpHeap() 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 return
} }
@@ -850,6 +856,9 @@ func ReadMemStats(m *MemStats) {
// Record the total allocated bytes. // Record the total allocated bytes.
m.TotalAlloc = gcTotalAlloc m.TotalAlloc = gcTotalAlloc
// Record the number of completed collection cycles.
m.NumGC = gcNumGC
gcLock.Unlock() gcLock.Unlock()
} }
+1
View File
@@ -129,6 +129,7 @@ func ReadMemStats(m *MemStats) {
m.Mallocs = 0 // not provided by bdwgc m.Mallocs = 0 // not provided by bdwgc
m.Frees = 0 // not provided by bdwgc m.Frees = 0 // not provided by bdwgc
m.Sys = uint64(gcMemStats.obtained_from_os_bytes) m.Sys = uint64(gcMemStats.obtained_from_os_bytes)
m.NumGC = uint32(gcMemStats.gc_no)
gcLock.Unlock() gcLock.Unlock()
} }
+1
View File
@@ -109,6 +109,7 @@ func ReadMemStats(m *MemStats) {
m.HeapAlloc = gcTotalAlloc m.HeapAlloc = gcTotalAlloc
m.HeapObjects = gcMallocs m.HeapObjects = gcMallocs
m.Alloc = m.HeapAlloc m.Alloc = m.HeapAlloc
m.NumGC = 0 // this GC never collects, so no cycle ever completes
gcLock.Unlock() gcLock.Unlock()
} }
+5
View File
@@ -82,4 +82,9 @@ type MemStats struct {
// GCSys is bytes of memory in garbage collection metadata. // GCSys is bytes of memory in garbage collection metadata.
GCSys uint64 GCSys uint64
// NumGC is the number of completed GC cycles.
//
// The leaking collector never collects, so it always reports 0.
NumGC uint32
} }