From 728dc6cd51cdfdd45db3a8402481c82c49bda970 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:47:17 -0500 Subject: [PATCH] Count program-lifetime objects in memoryUsage and fix the runtime tests - process.memoryUsage(): heapTotal counts the region's chunks that threads hold, minus the chunks discarded workers returned, and heapUsed counts them less the tails threads left unused when they moved to a new chunk. - The region builds only where it can be used: Linux runtimes with the size-class allocator, which excludes the sanitizer runtimes (scriptc keeps 8-byte references there). The standalone allocator, cycle and young-nursery tests link again: the allocator test defines the scr_trap its region needs, and checks the region (below 32 GiB, aligned, zeroed, chunks larger than one, heap statistics, threads). - scr_plr_chunk has an initializer, like the other runtime globals. --- packages/runtime/src/scr_alloc.c | 42 +++++++++++++++-- packages/runtime/src/scr_alloc.test.c | 67 +++++++++++++++++++++++++++ packages/runtime/src/scr_rc.h | 2 +- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 4b29b4b51..f2304aace 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -1930,6 +1930,10 @@ void scr_mem_order_for_free(void **ptrs, size_t n, size_t offset) { * mallinfo2, the Darwin malloc zones, the Windows CRT heap, or the * sanitizer's allocator in sanitized builds). Walks the free lists, so * the cost grows with the number of free blocks. */ +#if defined(__linux__) && SCR_SMALL_ALLOC +static void scr_plr_heap_stats(size_t *held, size_t *used); +#endif + void scr_heap_stats(double *total, double *used) { size_t held = 0, idle = 0; #if SCR_SMALL_ALLOC @@ -1979,6 +1983,13 @@ void scr_heap_stats(double *total, double *used) { sys_held = s.committed; sys_used = s.allocated; } +#endif +#if defined(__linux__) && SCR_SMALL_ALLOC + /* Compact class instances (the program-lifetime region below). */ + size_t region_held, region_used; + scr_plr_heap_stats(®ion_held, ®ion_used); + sys_held += region_held; + sys_used += region_used; #endif *total = (double)(held + sys_held); *used = (double)(held - (idle < held ? idle : held) + sys_used); @@ -1986,7 +1997,8 @@ void scr_heap_stats(double *total, double *used) { /* ── program-lifetime region ──────────────────────────────────────────── * Instances of compact classes (llvm/classes.ts LlClassMeta.compact) in - * programs built for Linux without sanitizers live in one address range + * programs built for Linux without sanitizers (runtimes with the size-class + * allocator, SCR_SMALL_ALLOC) live in one address range * reserved for the whole process below 32 GiB, so a field that holds one * can store its address over 8 in 32 bits (0 stays NULL). The instances * are never freed. Each thread carves them from its own chunks of @@ -1994,7 +2006,7 @@ void scr_heap_stats(double *total, double *used) { * Pages are committed by first touch and start zeroed. Its strings are * arrays rather than literals: programs without compact classes then link * none of this, where literals would stay in the unit's merged strings. */ -#if defined(__linux__) && SIZE_MAX > UINT32_MAX +#if defined(__linux__) && SCR_SMALL_ALLOC #include #define SCR_PLR_CHUNK ((size_t)1 << 20) @@ -2012,10 +2024,13 @@ void scr_heap_stats(double *total, double *used) { static char *scr_plr_base; /* the range's start, once reserved */ static size_t scr_plr_size; static size_t scr_plr_top; /* atomic */ +/* Bytes of the chunks threads hold, and of the tails they left unused when + * they moved to a new chunk (scr_heap_stats); atomic. */ +static size_t scr_plr_held, scr_plr_waste; #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -_Thread_local ScrPlrChunk scr_plr_chunk; +_Thread_local ScrPlrChunk scr_plr_chunk = {NULL, NULL}; #else -ScrPlrChunk scr_plr_chunk; +ScrPlrChunk scr_plr_chunk = {NULL, NULL}; #endif /* Reserves `size` bytes at `at` exactly; NULL if refused. */ @@ -2072,6 +2087,7 @@ static __attribute__((noinline)) void scr_plr_reserve(void) { * chunk a thread takes registers scr_plr_thread_end. */ #define SCR_PLR_LINK 16 static _Thread_local char *scr_plr_mine; +static _Thread_local size_t scr_plr_mine_waste; /* this thread's part of scr_plr_waste */ static pthread_key_t scr_plr_end_key; static pthread_once_t scr_plr_end_once = PTHREAD_ONCE_INIT; static pthread_mutex_t scr_plr_free_lock = PTHREAD_MUTEX_INITIALIZER; @@ -2112,6 +2128,9 @@ static void scr_plr_thread_end(void *unused) { c = next; } static const char stats_line[] = "scr_sa: thread returned %zu MB of program-lifetime objects\n"; + __atomic_fetch_sub(&scr_plr_held, returned, __ATOMIC_RELAXED); + __atomic_fetch_sub(&scr_plr_waste, scr_plr_mine_waste, __ATOMIC_RELAXED); + scr_plr_mine_waste = 0; const char *stats = getenv("SCR_SA_STATS"); if (stats && stats[0] == '1') fprintf(stderr, stats_line, returned >> 20); } @@ -2162,6 +2181,14 @@ static __attribute__((noinline)) void *scr_plr_refill(size_t n) { #ifdef SCR_SA_TRIM scr_plr_own(p, chunk); #endif + if (scr_plr_chunk.next) { + size_t left = (size_t)(scr_plr_chunk.limit - scr_plr_chunk.next); + __atomic_fetch_add(&scr_plr_waste, left, __ATOMIC_RELAXED); +#ifdef SCR_SA_TRIM + scr_plr_mine_waste += left; +#endif + } + __atomic_fetch_add(&scr_plr_held, chunk, __ATOMIC_RELAXED); scr_plr_chunk.next = p + SCR_PLR_LINK + n; scr_plr_chunk.limit = p + chunk; return p + SCR_PLR_LINK; @@ -2173,6 +2200,13 @@ void scr_plr_init(void) { if (!__atomic_load_n(&scr_plr_base, __ATOMIC_ACQUIRE)) scr_plr_reserve(); } +static void scr_plr_heap_stats(size_t *held, size_t *used) { + size_t h = __atomic_load_n(&scr_plr_held, __ATOMIC_RELAXED); + size_t w = __atomic_load_n(&scr_plr_waste, __ATOMIC_RELAXED); + *held = h; + *used = w < h ? h - w : 0; +} + void *scr_plr_alloc(size_t n) { n = (n + 7) & ~(size_t)7; char *p = scr_plr_chunk.next; diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index 2d8024c6d..e20c0733b 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -10,6 +10,13 @@ static bool owned(const void *p) { return (uintptr_t)p - scr_sa.base < scr_sa.span; } #endif +/* The allocator unit needs only scr_trap from the rest of the runtime: the + * program-lifetime region stops the program through it. */ +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + abort(); +} + static void fill(unsigned char *p, size_t n, unsigned char seed) { for (size_t i = 0; i < n; i++) p[i] = (unsigned char)(seed + i * 7); } @@ -962,6 +969,60 @@ static void check_discard(void) { } #endif +#if defined(__linux__) && SCR_SMALL_ALLOC +/* The program-lifetime region hands out zeroed, 8-byte aligned blocks below + * 32 GiB, in order from the calling thread's chunk; a block larger than a + * chunk gets chunks of its own. Heap statistics count the chunks. Builds + * with AddressSanitizer have no region (its shadow memory takes the range), + * and scriptc builds with sanitizers keep pointers instead. */ +static void check_program_lifetime_region(void) { + double total0, used0, total1, used1; + scr_heap_stats(&total0, &used0); + scr_plr_init(); + unsigned char *a = scr_plr_alloc(20), *b = scr_plr_alloc(40); + assert(((uintptr_t)a & 7) == 0 && b == a + 24); + assert((uintptr_t)a >= ((uintptr_t)4 << 30) && (uintptr_t)b + 40 <= ((uintptr_t)32 << 30)); + for (size_t i = 0; i < 40; i++) assert(b[i] == 0); + size_t n = (size_t)3 << 20; + unsigned char *big = scr_plr_alloc(n); + assert(((uintptr_t)big & 7) == 0 && (uintptr_t)big + n <= ((uintptr_t)32 << 30)); + for (size_t i = 0; i < n; i += 4096) assert(big[i] == 0); + fill(big, n, 3); + verify(big, n, 3); + scr_heap_stats(&total1, &used1); + assert(total1 - total0 >= (double)n && used1 - used0 >= (double)n && used1 <= total1); +} + +#ifdef SCR_WORKERS +/* Threads carve the region concurrently: every block is zeroed and keeps + * what its thread wrote. */ +#define REGION_THREADS 4u +#define REGION_BLOCKS 20000u +static unsigned char *region_blocks[REGION_THREADS][REGION_BLOCKS]; + +static void *region_alloc(void *arg) { + unsigned t = (unsigned)(uintptr_t)arg; + for (unsigned i = 0; i < REGION_BLOCKS; i++) { + unsigned char *p = scr_plr_alloc(24); + for (size_t k = 0; k < 24; k++) assert(p[k] == 0); + fill(p, 24, (unsigned char)(t * 31 + i)); + region_blocks[t][i] = p; + } + return NULL; +} + +static void check_program_lifetime_region_threads(void) { + pthread_t threads[REGION_THREADS]; + for (unsigned t = 0; t < REGION_THREADS; t++) + assert(pthread_create(&threads[t], NULL, region_alloc, (void *)(uintptr_t)t) == 0); + for (unsigned t = 0; t < REGION_THREADS; t++) assert(pthread_join(threads[t], NULL) == 0); + for (unsigned t = 0; t < REGION_THREADS; t++) + for (unsigned i = 0; i < REGION_BLOCKS; i++) + verify(region_blocks[t][i], 24, (unsigned char)(t * 31 + i)); +} +#endif +#endif + int main(void) { #if SCR_SMALL_ALLOC check_slice_growth(); /* first: its class must be untouched */ @@ -975,6 +1036,12 @@ int main(void) { #if SCR_SMALL_ALLOC && defined(__linux__) && defined(__GLIBC__) check_heap_huge_pages(); #endif +#if defined(__linux__) && SCR_SMALL_ALLOC + check_program_lifetime_region(); +#ifdef SCR_WORKERS + check_program_lifetime_region_threads(); +#endif +#endif #if defined(SCR_WORKERS) && SCR_SMALL_ALLOC /* A short discard delay, which stays at its start until check_discard_delay_grows. */ setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 93fdd7946..7a921b451 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -308,7 +308,7 @@ void scr_cyc_free(void *obj); /* frees the block, header included */ void *scr_mem_realloc(void *p, size_t n); void *scr_rt_calloc(size_t n); /* emitted-code entry points */ -#if defined(__linux__) && SIZE_MAX > UINT32_MAX +#if defined(__linux__) && SCR_SMALL_ALLOC /* The program-lifetime region (scr_alloc.c): compact class instances of * programs built for Linux without sanitizers (llvm/emitter.ts * lowRegionTarget), which are never freed, at 8-byte aligned addresses below