test: add test for gc stats
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <main> entry
|
||||
var (
|
||||
heapStart uintptr // start address of heap area
|
||||
heapEnd uintptr // end address of heap area
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user