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.
This commit is contained in:
Chris Tate
2026-10-22 00:47:17 -05:00
parent ecee5fdaaf
commit 728dc6cd51
3 changed files with 106 additions and 5 deletions
+38 -4
View File
@@ -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(&region_held, &region_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 <sys/mman.h>
#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;
+67
View File
@@ -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);
+1 -1
View File
@@ -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