From 36e84196c6e3cbea382fe3f006a95955095ccaff Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 14 Nov 2025 14:54:04 +0800 Subject: [PATCH] test: add test for gc stats --- runtime/internal/runtime/tinygogc/gc.go | 41 ++---- runtime/internal/runtime/tinygogc/gc_link.go | 25 ++++ runtime/internal/runtime/tinygogc/gc_test.go | 2 + .../internal/runtime/tinygogc/gc_tinygo.go | 1 - .../internal/runtime/tinygogc/pc_mock_test.go | 120 +++++++++++++++++- 5 files changed, 154 insertions(+), 35 deletions(-) diff --git a/runtime/internal/runtime/tinygogc/gc.go b/runtime/internal/runtime/tinygogc/gc.go index 965bc3bf..fd74479f 100644 --- a/runtime/internal/runtime/tinygogc/gc.go +++ b/runtime/internal/runtime/tinygogc/gc.go @@ -1,34 +1,9 @@ -//go:build baremetal && !testGC +//go:build baremetal || testGC package tinygogc import "unsafe" -// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation -// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead. -// This ensures all memory allocations go through the GC, including C library calls. -const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" - -//export __wrap_malloc -func __wrap_malloc(size uintptr) unsafe.Pointer { - return Alloc(size) -} - -//export __wrap_calloc -func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { - totalSize := nmemb * size - // Check for multiplication overflow - if nmemb != 0 && totalSize/nmemb != size { - return nil // Overflow - } - return Alloc(totalSize) -} - -//export __wrap_realloc -func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { - return Realloc(ptr, size) -} - type GCStats struct { // General statistics. @@ -189,16 +164,18 @@ func ReadGCStats() GCStats { stackSys := stackTop - stackEnd stats := GCStats{ - StackInuse: uint64(stackTop - uintptr(getsp())), - StackSys: uint64(stackSys), - HeapSys: heapInuse + heapIdle, - GCSys: uint64(heapEnd - uintptr(metadataStart)), + Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), TotalAlloc: gcTotalAlloc, + Sys: uint64(heapEnd - heapStart), Mallocs: gcMallocs, Frees: gcFrees, - Sys: uint64(heapEnd - heapStart), HeapAlloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), - Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock), + HeapSys: heapInuse + heapIdle, + HeapIdle: heapIdle, + HeapInuse: heapInuse, + StackInuse: uint64(stackTop - uintptr(getsp())), + StackSys: uint64(stackSys), + GCSys: uint64(heapEnd - uintptr(metadataStart)), } unlock(&gcMutex) diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 13ed3a74..9f5c04da 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -7,6 +7,31 @@ import ( _ "unsafe" ) +// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation +// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead. +// This ensures all memory allocations go through the GC, including C library calls. +const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc" + +//export __wrap_malloc +func __wrap_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __wrap_calloc +func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer { + totalSize := nmemb * size + // Check for multiplication overflow + if nmemb != 0 && totalSize/nmemb != size { + return nil // Overflow + } + return Alloc(totalSize) +} + +//export __wrap_realloc +func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer diff --git a/runtime/internal/runtime/tinygogc/gc_test.go b/runtime/internal/runtime/tinygogc/gc_test.go index 07fe7571..3ff26113 100644 --- a/runtime/internal/runtime/tinygogc/gc_test.go +++ b/runtime/internal/runtime/tinygogc/gc_test.go @@ -18,6 +18,8 @@ var _heapEnd [0]byte var _stackStart [0]byte +var _stackEnd [0]byte + var _globals_start [0]byte var _globals_end [0]byte diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 89fe320a..e4923d48 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -57,7 +57,6 @@ const blockStateByteAllTails = 0 | uint8(blockStateTail<<(stateBits*1)) | uint8(blockStateTail<<(stateBits*0)) -// since we don't have an init() function, these should be initalized by initHeap(), which is called by
entry var ( heapStart uintptr // start address of heap area heapEnd uintptr // end address of heap area diff --git a/runtime/internal/runtime/tinygogc/pc_mock_test.go b/runtime/internal/runtime/tinygogc/pc_mock_test.go index ba141c7d..c09bb9ff 100644 --- a/runtime/internal/runtime/tinygogc/pc_mock_test.go +++ b/runtime/internal/runtime/tinygogc/pc_mock_test.go @@ -99,9 +99,17 @@ func (env *mockGCEnv) setupMockGC() { // Clear metadata using memset like initGC does c.Memset(metadataStart, 0, metadataSize) - // Reset allocator state (initGC doesn't reset these, but we need to) + // Reset allocator state and all GC statistics for clean test environment nextAlloc = 0 isGCInit = true + + // Reset all GC statistics to start from clean state + gcTotalAlloc = 0 + gcTotalBlocks = 0 + gcMallocs = 0 + gcFrees = 0 + gcFreedBlocks = 0 + markStackOverflow = false } // restoreOriginalGC restores the original GC state @@ -114,7 +122,7 @@ func (env *mockGCEnv) restoreOriginalGC() { endBlock = env.originalEndBlock metadataStart = env.originalMetadataStart nextAlloc = env.originalNextAlloc - isGCInit = env.originalIsGCInit + isGCInit = false } // enableMockMode enables mock root scanning mode @@ -404,6 +412,114 @@ func TestMockGCMemoryPressure(t *testing.T) { t.Log("Successfully allocated more objects after GC") } +func TestMockGCStats(t *testing.T) { + env := createMockGCEnv() + env.setupMockGC() + defer env.restoreOriginalGC() + + // Get initial stats + initialStats := ReadGCStats() + t.Logf("Initial stats - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + initialStats.Mallocs, initialStats.Frees, initialStats.TotalAlloc, initialStats.Alloc) + + // Verify basic system stats + expectedSys := uint64(env.heapEnd - env.heapStart - 2048) + if initialStats.Sys != expectedSys { + t.Errorf("Expected Sys %d, got %d", expectedSys, initialStats.Sys) + } + + expectedGCSys := uint64(env.heapEnd - uintptr(metadataStart)) + if initialStats.GCSys != expectedGCSys { + t.Errorf("Expected GCSys %d, got %d", expectedGCSys, initialStats.GCSys) + } + + // Allocate some objects + var allocations []unsafe.Pointer + allocSize := uintptr(64) + numAllocs := 10 + + for i := 0; i < numAllocs; i++ { + ptr := Alloc(allocSize) + if ptr == nil { + t.Fatalf("Failed to allocate at iteration %d", i) + } + allocations = append(allocations, ptr) + } + + // Check stats after allocation + afterAllocStats := ReadGCStats() + t.Logf("After allocation - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + afterAllocStats.Mallocs, afterAllocStats.Frees, afterAllocStats.TotalAlloc, afterAllocStats.Alloc) + + // Verify allocation stats increased + if afterAllocStats.Mallocs <= initialStats.Mallocs { + t.Errorf("Expected Mallocs to increase from %d, got %d", initialStats.Mallocs, afterAllocStats.Mallocs) + } + + if afterAllocStats.TotalAlloc <= initialStats.TotalAlloc { + t.Errorf("Expected TotalAlloc to increase from %d, got %d", initialStats.TotalAlloc, afterAllocStats.TotalAlloc) + } + + if afterAllocStats.Alloc <= initialStats.Alloc { + t.Errorf("Expected Alloc to increase from %d, got %d", initialStats.Alloc, afterAllocStats.Alloc) + } + + // Verify Alloc and HeapAlloc are the same + if afterAllocStats.Alloc != afterAllocStats.HeapAlloc { + t.Errorf("Expected Alloc (%d) to equal HeapAlloc (%d)", afterAllocStats.Alloc, afterAllocStats.HeapAlloc) + } + + // Perform GC with controlled roots - keep only half the allocations + env.enableMockMode() + keepCount := len(allocations) / 2 + for i := 0; i < keepCount; i++ { + env.addRoot(allocations[i]) + } + + freedBytes := env.runMockGC() + t.Logf("GC freed %d bytes", freedBytes) + + // Check stats after GC + afterGCStats := ReadGCStats() + t.Logf("After GC - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d", + afterGCStats.Mallocs, afterGCStats.Frees, afterGCStats.TotalAlloc, afterGCStats.Alloc) + + // Verify GC stats + if afterGCStats.Frees <= afterAllocStats.Frees { + t.Errorf("Expected Frees to increase from %d, got %d", afterAllocStats.Frees, afterGCStats.Frees) + } + + // TotalAlloc should not decrease (cumulative) + if afterGCStats.TotalAlloc != afterAllocStats.TotalAlloc { + t.Errorf("Expected TotalAlloc to remain %d after GC, got %d", afterAllocStats.TotalAlloc, afterGCStats.TotalAlloc) + } + + // Alloc should decrease (freed objects) + if afterGCStats.Alloc >= afterAllocStats.Alloc { + t.Errorf("Expected Alloc to decrease from %d after GC, got %d", afterAllocStats.Alloc, afterGCStats.Alloc) + } + + // Verify heap statistics consistency + if afterGCStats.HeapSys != afterGCStats.HeapInuse+afterGCStats.HeapIdle { + t.Errorf("Expected HeapSys (%d) to equal HeapInuse (%d) + HeapIdle (%d)", + afterGCStats.HeapSys, afterGCStats.HeapInuse, afterGCStats.HeapIdle) + } + + // Verify live objects calculation + expectedLiveObjects := afterGCStats.Mallocs - afterGCStats.Frees + t.Logf("Live objects: %d (Mallocs: %d - Frees: %d)", expectedLiveObjects, afterGCStats.Mallocs, afterGCStats.Frees) + + // The number of live objects should be reasonable (we kept half the allocations plus some overhead) + if expectedLiveObjects < uint64(keepCount) { + t.Errorf("Expected at least %d live objects, got %d", keepCount, expectedLiveObjects) + } + + // Test stack statistics + if afterGCStats.StackInuse > afterGCStats.StackSys { + t.Errorf("StackInuse (%d) should not exceed StackSys (%d)", afterGCStats.StackInuse, afterGCStats.StackSys) + } +} + func TestMockGCCircularReferences(t *testing.T) { env := createMockGCEnv() env.setupMockGC()