Files
scriptc/packages/runtime/src/scr_alloc.c
T

1084 lines
43 KiB
C

/* scriptc runtime — small-object allocator state and slow paths.
*
* The fast paths (free-list pop, bump carve, free-list push) are inline in
* scr_runtime.h; see the contract there. This unit owns the process-wide
* state, the address-space reservation, the refill/fallback path, realloc,
* and the out-of-line entry points the LLVM backend calls for acyclic
* records and class instances.
*
* Freed blocks are recycled within their class; the reservation itself
* costs address space only, and pages are committed as the bump pointers
* first touch them. Only an exiting worker thread returns pages, those
* holding nothing but free blocks (scr_sa_trim, Linux). */
#include "scr_runtime.h"
/* Defined in every build: the emitted fast paths read it (see the layout
* contract in scr_runtime.h); without the allocator it stays all-zero.
* Explicitly initialized so it is an ordinary (localizable) definition: a
* tentative definition becomes a COMMON symbol, which library localization
* deliberately keeps global (abi.localize_runtime), leaking it from every
* localized library archive. */
#ifdef SCR_WORKERS
_Thread_local ScrThreadHot scr_thread_hot = {{0}};
#else
ScrSaState scr_sa = {0};
#endif
#if SCR_SMALL_ALLOC
#include <sys/mman.h>
/* Page policy. Slice pages are committed by first touch, which on 4 KiB
* pages is one fault (and page clear) per 4 KiB the bump pointers cross.
* Transparent huge pages fault 2 MiB at a time and shrink the TLB working
* set, but make the whole 2 MiB page around a bump pointer resident at once,
* so every class slice in use (per arena, in worker executables) would cost
* up to 2 MiB more than it holds. A slice therefore starts on small pages
* and moves the rest of its range to huge pages once it has carved
* SCR_SA_HUGE_AFTER bytes: only slices that already hold that much pay the
* partial last huge page. The reservation is huge-page aligned so the
* switch point and every slice start on a huge-page boundary. */
#if defined(__linux__) && defined(MADV_HUGEPAGE) && defined(MADV_NOHUGEPAGE)
#define SCR_SA_THP 1
#define SCR_SA_HUGE_PAGE ((size_t)2 << 20)
#define SCR_SA_HUGE_AFTER ((size_t)8 << 20)
#endif
#if defined(SCR_SA_THP) && defined(__GLIBC__)
#include <stdio.h>
#include <string.h>
/* The system's THP mode: 'a' (always), 'm' (madvise), 'n' (never), or 0
* when the kernel has no THP setting to read. */
static char scr_sa_thp_mode(void) {
FILE *f = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r");
if (!f) return 0;
char buf[96] = {0};
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
if (strstr(buf, "[always]")) return 'a';
if (strstr(buf, "[madvise]")) return 'm';
return strstr(buf, "[never]") ? 'n' : 0;
}
#endif
/* Core dumps. The reservation is tens of GiB of address space, almost none
* of it ever touched. A core written to a file skips untouched pages, but a
* core piped to a handler (a kernel.core_pattern of "|...", such as apport
* or systemd-coredump) carries every page of a dumped mapping, untouched
* ones as zeros, and RLIMIT_CORE does not bound it: an aborting program
* would take seconds to minutes to die. The reservation is therefore mapped
* MADV_DONTDUMP, and whenever the slow path raises a slice's bump limit,
* the slice up to that limit becomes dumpable again (scr_sa_dumpable), so a
* core still holds every carved block. Without pre-faulting, limits advance
* in bounded steps for this (SCR_SA_DUMP_STEP on small pages, one huge page
* past the switch point). The dumpable part ends on a grid that coarsens as
* the slice grows (scr_sa_dump_edge): its untouched tail is at most
* SCR_SA_DUMP_GRAIN or a quarter of what the slice has carved, and a slice
* makes a few madvise calls per doubling, not one per step (each takes the
* mmap lock for writing, which would wait for the pre-fault helper). Grid
* points are huge-page aligned: a mapping boundary inside a huge page would
* split it. */
#if defined(__linux__) && defined(MADV_DONTDUMP)
#define SCR_SA_DUMP 1
#define SCR_SA_DUMP_STEP ((size_t)1 << 20)
#define SCR_SA_DUMP_GRAIN ((size_t)2 << 20) /* a multiple of the page size */
#endif
/* Maps span bytes of address space for slices (huge-page aligned where the
* policy above applies), or NULL. */
static void *scr_sa_map(size_t span) {
int flags = MAP_PRIVATE | MAP_ANON;
#ifdef MAP_NORESERVE
flags |= MAP_NORESERVE;
#endif
#ifdef SCR_SA_THP
size_t pad = SCR_SA_HUGE_PAGE;
#else
size_t pad = 0;
#endif
char *p = mmap(NULL, span + pad, PROT_READ | PROT_WRITE, flags, -1, 0);
if (p == MAP_FAILED) return NULL;
#ifdef SCR_SA_THP
char *a = (char *)(((uintptr_t)p + pad - 1) & ~(uintptr_t)(pad - 1));
if (a > p) (void)munmap(p, (size_t)(a - p));
(void)munmap(a + span, (size_t)(p + pad - a)); /* a < p + pad */
p = a;
(void)madvise(p, span, MADV_NOHUGEPAGE);
#endif
#ifdef SCR_SA_DUMP
(void)madvise(p, span, MADV_DONTDUMP);
#endif
return p;
}
#ifdef SCR_SA_DUMP
/* The end of the dumpable part of the class slice [start, end) whose
* limit is p: p rounded up to a multiple of SCR_SA_DUMP_GRAIN or of a
* quarter of the largest power of two at or below p - start, whichever is
* larger (see above). Monotonic in p. */
static char *scr_sa_dump_edge(char *start, char *end, char *p) {
size_t off = (size_t)(p - start), grain = SCR_SA_DUMP_GRAIN;
if (off) {
size_t quarter = ((size_t)1 << (63 - __builtin_clzll((unsigned long long)off))) >> 2;
if (quarter > grain) grain = quarter;
}
off = (off + grain - 1) & ~(grain - 1);
return off < (size_t)(end - start) ? start + off : end;
}
/* The slice [start, end) raised its bump limit from lim to next: makes
* the memory up to next dumpable (see above). */
static void scr_sa_dumpable(char *start, char *end, char *lim, char *next) {
char *lo = scr_sa_dump_edge(start, end, lim), *hi = scr_sa_dump_edge(start, end, next);
if (hi > lo) (void)madvise(lo, (size_t)(hi - lo), MADV_DODUMP);
}
#endif
/* Pre-faulting (Linux executables). First touch of a fresh slice page costs
* a fault and a page clear on the allocating thread. With pre-faulting the
* bump limits advance in steps, and each step asks a helper thread to fault
* in the memory just ahead of it (MADV_POPULATE_WRITE: the kernel commits
* zeroed pages as for a write, without storing anything, so it cannot race
* with the owner's carving). The owner still faults any page the helper has
* not reached; the fast paths are unchanged. Small-page prefixes advance in
* SCR_SA_PF_STEP chunks and fault one chunk ahead; huge-page ranges advance
* in half huge pages and fault the next huge page once the bump is in the
* second half of the current one, so a slice keeps at most about one step
* of untouched committed memory. Nothing is faulted ahead before a slice
* reaches SCR_SA_PF_START: a lightly used class commits nothing extra.
* SCR_SA_PREFAULT=0 turns it off. Library archives run on embedder threads
* and keep the plain policy. */
#if defined(SCR_SA_THP) && !defined(SCR_LIB)
#ifndef MADV_POPULATE_WRITE
#define MADV_POPULATE_WRITE 23 /* Linux 5.14 */
#endif
#define SCR_SA_PREFAULT 1
#define SCR_SA_PF_STEP ((size_t)256 << 10)
#define SCR_SA_PF_START ((size_t)1 << 20)
#define SCR_SA_PF_HUGE_STEP (SCR_SA_HUGE_PAGE / 2)
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
typedef struct {
char *lo, *hi;
} ScrSaPfRange;
#define SCR_SA_PF_RING 64u
static pthread_mutex_t scr_sa_pf_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t scr_sa_pf_cond = PTHREAD_COND_INITIALIZER;
static ScrSaPfRange scr_sa_pf_ring[SCR_SA_PF_RING];
static unsigned scr_sa_pf_head, scr_sa_pf_tail; /* under the lock */
static bool scr_sa_pf_started, scr_sa_pf_waiting;
/* 0 = not decided yet, 1 = on, -1 = off (environment, no thread, or a
* kernel without MADV_POPULATE_WRITE). */
static _Atomic(int) scr_sa_pf_state;
static void scr_sa_pf_populate(char *lo, char *hi) {
if (madvise(lo, (size_t)(hi - lo), MADV_POPULATE_WRITE) != 0 && errno == EINVAL)
atomic_store_explicit(&scr_sa_pf_state, -1, memory_order_relaxed);
}
/* The system allocator's heap (glibc): blocks above SCR_SA_MAX come from
* malloc, whose main arena grows by brk. Once the heap has grown by
* SCR_SA_PF_HEAP_MIN since the helper first looked, the helper gives malloc
* a larger top pad (SCR_SA_PF_TOP_PAD), so every extension runs that far
* ahead of use, and faults in each new extension as soon as it sees the
* break move. Smaller heaps keep glibc's defaults (a top pad also fixes
* glibc's otherwise dynamic mmap threshold), so a small program commits
* nothing extra. The helper checks after every request and then polls while
* the program keeps allocating (SCR_SA_PF_POLL_NS, up to SCR_SA_PF_POLLS
* times without growth before it sleeps until the next request), so an idle
* program does not wake it. Only the helper reads the break; a stale read
* only delays a fault-in, and a shrunk break (malloc trimming its top)
* restarts the watermarks there. Past the threshold the pad costs at most
* SCR_SA_PF_TOP_PAD of committed memory beyond what malloc would have;
* SCR_SA_PREFAULT_HEAP=0 keeps glibc's defaults and no polling.
*
* Where THP is in madvise mode none of this runs. The heap policy below
* moves the heap to huge pages at the same 32 MiB, so the owner takes one
* fault per 2 MiB and faulting ahead saves it little, while the helper's
* populates cost more: ranges it reaches before the owner advises them (and
* the partial huge pages at the ends of each extension) stay on small pages,
* 230-620 MB on large type checks; the owner's brk and madvise
* wait for the mmap lock a multi-megabyte populate holds; and the helper's
* top pad would replace the heap policy's larger one. */
#if defined(__GLIBC__)
#include <malloc.h>
#include <time.h>
#include <unistd.h>
#define SCR_SA_PF_HEAP 1
#define SCR_SA_PF_HEAP_MIN ((size_t)32 << 20)
#define SCR_SA_PF_TOP_PAD ((size_t)16 << 20)
#define SCR_SA_PF_POLL_NS 1000000L
#define SCR_SA_PF_POLLS 64u
static int scr_sa_pf_heap_mode; /* 0 off, 1 on; decided before the helper starts */
static uintptr_t scr_sa_pf_page; /* likewise */
static char *scr_sa_pf_brk0, *scr_sa_pf_brk; /* helper only: first and last break seen */
static bool scr_sa_pf_heap_big; /* helper only: past SCR_SA_PF_HEAP_MIN */
/* Faults in the heap's growth since the last check; true if it grew. */
static bool scr_sa_pf_heap(void) {
char *cur = sbrk(0);
if (cur == (char *)-1) return false;
char *seen = scr_sa_pf_brk;
scr_sa_pf_brk = cur;
if (!seen || cur < scr_sa_pf_brk0) scr_sa_pf_brk0 = cur;
if (!seen || cur <= seen) return false;
if (!scr_sa_pf_heap_big) {
if ((size_t)(cur - scr_sa_pf_brk0) < SCR_SA_PF_HEAP_MIN) return true;
scr_sa_pf_heap_big = true;
(void)mallopt(M_TOP_PAD, (int)SCR_SA_PF_TOP_PAD);
}
scr_sa_pf_populate((char *)((uintptr_t)seen & ~(scr_sa_pf_page - 1)), cur);
return true;
}
#endif
static void *scr_sa_pf_main(void *arg) {
(void)arg;
#ifdef SCR_SA_PF_HEAP
unsigned idle = SCR_SA_PF_POLLS;
#endif
pthread_mutex_lock(&scr_sa_pf_lock);
for (;;) {
while (scr_sa_pf_head == scr_sa_pf_tail) {
scr_sa_pf_waiting = true;
#ifdef SCR_SA_PF_HEAP
if (scr_sa_pf_heap_mode && idle < SCR_SA_PF_POLLS) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
t.tv_nsec += SCR_SA_PF_POLL_NS;
if (t.tv_nsec >= 1000000000L) {
t.tv_sec++;
t.tv_nsec -= 1000000000L;
}
int rc = pthread_cond_timedwait(&scr_sa_pf_cond, &scr_sa_pf_lock, &t);
scr_sa_pf_waiting = false;
if (rc != 0 && scr_sa_pf_head == scr_sa_pf_tail) {
pthread_mutex_unlock(&scr_sa_pf_lock);
idle = scr_sa_pf_heap() ? 0 : idle + 1;
pthread_mutex_lock(&scr_sa_pf_lock);
}
continue;
}
#endif
pthread_cond_wait(&scr_sa_pf_cond, &scr_sa_pf_lock);
scr_sa_pf_waiting = false;
}
ScrSaPfRange r = scr_sa_pf_ring[scr_sa_pf_tail++ % SCR_SA_PF_RING];
pthread_mutex_unlock(&scr_sa_pf_lock);
scr_sa_pf_populate(r.lo, r.hi);
#ifdef SCR_SA_PF_HEAP
if (scr_sa_pf_heap_mode) {
(void)scr_sa_pf_heap();
idle = 0;
}
#endif
pthread_mutex_lock(&scr_sa_pf_lock);
}
return NULL;
}
static bool scr_sa_pf_on(void) {
int s = atomic_load_explicit(&scr_sa_pf_state, memory_order_relaxed);
if (s == 0) {
const char *v = getenv("SCR_SA_PREFAULT");
s = v && v[0] == '0' ? -1 : 1;
atomic_store_explicit(&scr_sa_pf_state, s, memory_order_relaxed);
}
return s > 0;
}
/* Asks the helper to fault in [lo, hi) (page aligned). Dropped when the
* ring is full: the owner then faults those pages itself. */
static void scr_sa_prefault(char *lo, char *hi) {
if (hi <= lo || !scr_sa_pf_on()) return;
pthread_mutex_lock(&scr_sa_pf_lock);
if (!scr_sa_pf_started) {
scr_sa_pf_started = true;
#ifdef SCR_SA_PF_HEAP
/* Nobody waits on the condition yet: give it the monotonic clock for
* the helper's polls (no polls without it). */
const char *v = getenv("SCR_SA_PREFAULT_HEAP");
long page = sysconf(_SC_PAGESIZE);
char thp = scr_sa_thp_mode();
pthread_condattr_t ca;
if (!(v && v[0] == '0') && thp != 'm' && page > 0 && pthread_condattr_init(&ca) == 0) {
if (pthread_condattr_setclock(&ca, CLOCK_MONOTONIC) == 0) {
pthread_cond_destroy(&scr_sa_pf_cond);
pthread_cond_init(&scr_sa_pf_cond, &ca);
scr_sa_pf_page = (uintptr_t)page;
scr_sa_pf_heap_mode = 1;
}
pthread_condattr_destroy(&ca);
}
#endif
/* The helper takes no signals: they stay with the program's threads. */
pthread_attr_t attr;
pthread_t helper;
sigset_t all, old;
sigfillset(&all);
bool ok = pthread_attr_init(&attr) == 0;
if (ok) {
bool masked = pthread_sigmask(SIG_SETMASK, &all, &old) == 0;
ok = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 &&
pthread_attr_setstacksize(&attr, (size_t)64 << 10) == 0 &&
pthread_create(&helper, &attr, scr_sa_pf_main, NULL) == 0;
if (masked) (void)pthread_sigmask(SIG_SETMASK, &old, NULL);
pthread_attr_destroy(&attr);
}
if (!ok) atomic_store_explicit(&scr_sa_pf_state, -1, memory_order_relaxed);
}
if (atomic_load_explicit(&scr_sa_pf_state, memory_order_relaxed) > 0 &&
scr_sa_pf_head - scr_sa_pf_tail < SCR_SA_PF_RING) {
scr_sa_pf_ring[scr_sa_pf_head++ % SCR_SA_PF_RING] = (ScrSaPfRange){lo, hi};
if (scr_sa_pf_waiting) pthread_cond_signal(&scr_sa_pf_cond);
}
pthread_mutex_unlock(&scr_sa_pf_lock);
}
#endif
/* The next bump limit of a class slice [start, end) whose limit is lim
* (< end): its small-page prefix ends at SCR_SA_HUGE_AFTER, where the rest
* of the slice switches to huge pages. Limits advance in steps: the
* pre-fault steps (see above), or else the core-dump steps (SCR_SA_DUMP). */
static char *scr_sa_extend(char *start, char *lim, char *end) {
#ifdef SCR_SA_THP
char *sw = start + SCR_SA_HUGE_AFTER;
bool tiered = (size_t)(end - start) > SCR_SA_HUGE_AFTER;
#ifdef SCR_SA_PREFAULT
if (scr_sa_pf_on()) {
if (!tiered) return end;
if (lim < sw) {
char *next = lim + SCR_SA_PF_STEP < sw ? lim + SCR_SA_PF_STEP : sw;
if (next == sw) {
/* The last small-page chunk: switch the rest now, so the first huge
* page can be faulted ahead like every later one. */
(void)madvise(sw, (size_t)(end - sw), MADV_HUGEPAGE);
scr_sa_prefault(sw, sw + SCR_SA_HUGE_PAGE < end ? sw + SCR_SA_HUGE_PAGE : end);
} else if ((size_t)(next - start) >= SCR_SA_PF_START) {
scr_sa_prefault(next, next + SCR_SA_PF_STEP < sw ? next + SCR_SA_PF_STEP : sw);
}
return next;
}
char *next = lim + SCR_SA_PF_HUGE_STEP < end ? lim + SCR_SA_PF_HUGE_STEP : end;
/* Entering the second half of a huge page: fault the next one. */
if (((uintptr_t)next & (SCR_SA_HUGE_PAGE - 1)) == 0 && next < end)
scr_sa_prefault(next, next + SCR_SA_HUGE_PAGE < end ? next + SCR_SA_HUGE_PAGE : end);
return next;
}
#endif
if (tiered && lim < sw) {
#ifdef SCR_SA_DUMP
if ((size_t)(sw - lim) > SCR_SA_DUMP_STEP) return lim + SCR_SA_DUMP_STEP;
#endif
return sw;
}
if (tiered && lim == sw) (void)madvise(sw, (size_t)(end - sw), MADV_HUGEPAGE);
#ifdef SCR_SA_DUMP
if (tiered && (size_t)(end - lim) > SCR_SA_HUGE_PAGE) return lim + SCR_SA_HUGE_PAGE;
#endif
#else
(void)start;
#ifdef SCR_SA_DUMP
if ((size_t)(end - lim) > SCR_SA_DUMP_STEP) return lim + SCR_SA_DUMP_STEP;
#else
(void)lim;
#endif
#endif
return end;
}
/* Blocks above SCR_SA_MAX (array storage, hash tables, link pages, large
* strings) come from glibc, whose heap and per-block mappings use small
* pages where THP is in madvise mode (the common distribution default);
* in large compiler-like programs that is most of the page faults and a
* share of the TLB misses. glibc only advises huge pages for its own
* memory when the glibc.malloc.hugetlb tunable is set in the environment,
* which a program cannot do for itself. So once the brk heap has grown
* past SCR_SA_HEAP_HUGE_AFTER (small programs keep small pages and their
* RSS), every large allocation advises the heap grown since the previous
* one, and the aligned interior of a block mapped outside the heap.
* glibc grows the heap by its top pad beyond each request; the default
* (128 KiB) is smaller than a huge page, so every 2 MiB range would be
* first touched while it still straddles the heap end, where it can only
* get small pages. The policy therefore raises the top pad. Setting it
* fixes glibc's otherwise adaptive mmap and trim thresholds, so they are
* set to the adaptive maxima the heap of such a program reaches anyway. */
#if defined(SCR_SA_THP) && defined(__GLIBC__)
#include <malloc.h>
#include <stdatomic.h>
#include <unistd.h>
#define SCR_SA_HEAP_HUGE_AFTER ((uintptr_t)32 << 20)
#define SCR_SA_HEAP_TOP_PAD (32 << 20)
#define SCR_SA_HEAP_MMAP_THRESHOLD (32 << 20) /* glibc's adaptive maximum */
#define SCR_SA_HEAP_TRIM_THRESHOLD (64 << 20) /* twice that, as glibc keeps it */
/* 0 = undecided, 1 = following, 2 = off (THP not in madvise mode). */
static _Atomic(int) scr_sa_heap_state;
static _Atomic(uintptr_t) scr_sa_heap_lo; /* heap start seen at the first large block */
static _Atomic(uintptr_t) scr_sa_heap_hi; /* end of the advised heap prefix */
static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode */
/* The page size is stored before scr_sa_heap_lo is published (the CAS is a
* release), and every thread loads scr_sa_heap_lo, then scr_sa_heap_hi, with
* acquire before reading it; each scr_sa_heap_hi update is an RMW by a thread
* that already sees it, so any thread that wins the advice CAS sees a nonzero
* page size. A zero read still skips the advice rather than align to 0.
* These loads run only on blocks too large for the small allocator, next to
* a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */
static void scr_sa_large_note(void *p, size_t n) {
int state = atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed);
if (state == 2 || !p) return;
uintptr_t top = (uintptr_t)sbrk(0);
if (state == 0) {
uintptr_t lo = atomic_load_explicit(&scr_sa_heap_lo, memory_order_acquire);
if (!lo) {
long page = sysconf(_SC_PAGESIZE);
if (page <= 0 || scr_sa_thp_mode() != 'm') {
atomic_store_explicit(&scr_sa_heap_state, 2, memory_order_relaxed);
return;
}
atomic_store_explicit(&scr_sa_heap_page, (uintptr_t)page, memory_order_relaxed);
lo = (top + (uintptr_t)page - 1) & ~((uintptr_t)page - 1);
uintptr_t none = 0;
if (!atomic_compare_exchange_strong(&scr_sa_heap_lo, &none, lo)) lo = none;
}
if (top < lo + SCR_SA_HEAP_HUGE_AFTER) return;
uintptr_t unset = 0; /* the whole heap seen so far gets the advice */
(void)atomic_compare_exchange_strong(&scr_sa_heap_hi, &unset, lo);
int undecided = 0;
if (atomic_compare_exchange_strong(&scr_sa_heap_state, &undecided, 1)) {
(void)mallopt(M_MMAP_THRESHOLD, SCR_SA_HEAP_MMAP_THRESHOLD);
(void)mallopt(M_TRIM_THRESHOLD, SCR_SA_HEAP_TRIM_THRESHOLD);
(void)mallopt(M_TOP_PAD, SCR_SA_HEAP_TOP_PAD);
}
}
/* The heap end moved since the last look: advise what it gained (pages
* already touched keep their small pages), or follow a trim down. */
uintptr_t hi = atomic_load_explicit(&scr_sa_heap_hi, memory_order_acquire);
if (!hi) return; /* another thread is switching the policy on */
if (top > hi) {
if (atomic_compare_exchange_strong(&scr_sa_heap_hi, &hi, top)) {
/* madvise wants a page-aligned start */
uintptr_t page = atomic_load_explicit(&scr_sa_heap_page, memory_order_relaxed);
if (page) {
uintptr_t from = hi & ~(page - 1);
(void)madvise((void *)from, top - from, MADV_HUGEPAGE);
}
}
} else if (top < hi) {
(void)atomic_compare_exchange_strong(&scr_sa_heap_hi, &hi, top);
}
/* A block glibc mapped by itself (or carved from a thread's heap). */
uintptr_t a = (uintptr_t)p, lo = atomic_load_explicit(&scr_sa_heap_lo, memory_order_relaxed);
if ((a < lo || a >= top) && n >= 2 * SCR_SA_HUGE_PAGE) {
uintptr_t s = (a + SCR_SA_HUGE_PAGE - 1) & ~(uintptr_t)(SCR_SA_HUGE_PAGE - 1);
uintptr_t e = (a + n) & ~(uintptr_t)(SCR_SA_HUGE_PAGE - 1);
if (e > s) (void)madvise((void *)s, e - s, MADV_HUGEPAGE);
}
}
#define SCR_SA_LARGE_NOTE(p, n) scr_sa_large_note((p), (n))
#else
#define SCR_SA_LARGE_NOTE(p, n) ((void)0)
#endif
#ifdef SCR_WORKERS
#include <pthread.h>
#include <stdatomic.h>
#if defined(__linux__) && defined(MADV_DONTNEED)
#define SCR_SA_TRIM 1
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#if defined(__GLIBC__)
#include <malloc.h>
#endif
#endif
/* Per-thread arenas (contract in scr_runtime.h). The region is reserved
* once; arena i spans [region + i * arena_span, + arena_span) and is split
* into class slices exactly like the single-threaded reservation. */
#define SCR_SA_ARENAS 32u
static pthread_mutex_t scr_sa_region_lock = PTHREAD_MUTEX_INITIALIZER;
static _Atomic(uintptr_t) scr_sa_region; /* 0 until reserved */
static uintptr_t scr_sa_arena_span;
static unsigned scr_sa_region_shift;
static bool scr_sa_region_refused;
static _Atomic(uint32_t) scr_sa_claimed; /* one bit per arena */
static ScrSaState scr_sa_parked[SCR_SA_ARENAS]; /* unowned arenas */
static _Atomic(ScrSaBlock *) scr_sa_remote[SCR_SA_ARENAS]; /* foreign frees */
static _Thread_local int scr_sa_arena = -1;
static _Thread_local bool scr_sa_refused; /* this thread got no arena */
_Static_assert(SCR_SA_ARENAS <= 32, "one claim bit per arena");
static bool scr_sa_reserve_region(void) {
pthread_mutex_lock(&scr_sa_region_lock);
if (!atomic_load_explicit(&scr_sa_region, memory_order_acquire) && !scr_sa_region_refused) {
/* Same per-class slice ladder as the single-threaded reservation, for
* every arena at once; hosts that limit address space get smaller
* slices (each slice that fills falls back to the system). */
for (unsigned shift = 30; shift >= 22; shift -= 2) {
size_t arena = (size_t)SCR_SA_NCLASS << shift;
size_t span = arena * SCR_SA_ARENAS;
void *p = scr_sa_map(span);
if (!p) continue;
scr_sa_arena_span = arena;
scr_sa_region_shift = shift;
atomic_store_explicit(&scr_sa_region, (uintptr_t)p, memory_order_release);
break;
}
if (!atomic_load_explicit(&scr_sa_region, memory_order_relaxed)) scr_sa_region_refused = true;
}
pthread_mutex_unlock(&scr_sa_region_lock);
return atomic_load_explicit(&scr_sa_region, memory_order_acquire) != 0;
}
#ifdef SCR_SA_TRIM
/* The arena's carved bytes when the current thread claimed it (see
* scr_sa_trim). */
static size_t scr_sa_claim_carved[SCR_SA_ARENAS];
static size_t scr_sa_carved(void);
#endif
/* Moves every block other threads freed into this arena's free lists. */
static void scr_sa_drain_remote(void) {
ScrSaBlock *b = atomic_exchange_explicit(&scr_sa_remote[scr_sa_arena], NULL, memory_order_acquire);
while (b) {
ScrSaBlock *next = b->next;
unsigned c = (unsigned)(((uintptr_t)b - scr_sa.base) >> scr_sa.shift);
b->next = scr_sa.free[c];
scr_sa.free[c] = b;
b = next;
}
}
/* Claims an arena for the calling thread: a parked one continues with its
* free lists and bump pointers, a fresh one starts empty. */
static bool scr_sa_reserve(void) {
if (!atomic_load_explicit(&scr_sa_region, memory_order_acquire) && !scr_sa_reserve_region()) {
scr_sa_refused = true;
return false;
}
uintptr_t region = atomic_load_explicit(&scr_sa_region, memory_order_acquire);
uint32_t claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed);
for (;;) {
if (claimed == UINT32_MAX) {
scr_sa_refused = true;
return false;
}
unsigned i = (unsigned)__builtin_ctz(~claimed);
if (atomic_compare_exchange_weak_explicit(&scr_sa_claimed, &claimed, claimed | (1u << i),
memory_order_acquire, memory_order_relaxed)) {
scr_sa_arena = (int)i;
scr_sa = scr_sa_parked[i];
if (!scr_sa.span) {
memset(&scr_sa, 0, sizeof scr_sa);
scr_sa.base = region + (uintptr_t)i * scr_sa_arena_span;
scr_sa.span = scr_sa_arena_span;
scr_sa.shift = scr_sa_region_shift;
}
scr_sa_drain_remote();
#ifdef SCR_SA_TRIM
scr_sa_claim_carved[i] = scr_sa_carved();
#endif
return true;
}
}
}
#ifdef SCR_SA_TRIM
/* Returning an exited thread's free memory (Linux). When a script thread
* exits, every page of its arena that holds only free blocks goes back to
* the system (MADV_DONTNEED, after which it reads as zero), so other
* threads can use that memory while the arena is parked. The free blocks on
* the kept pages are relinked in address order; the returned runs of pages
* become zeroed carve regions (`ScrSaHoles`), which the next thread to claim
* the arena carves before the slice's own frontier: the bump fast paths
* already rely on carved memory being zero. A thread that carved less than
* SCR_SA_TRIM_MIN, or whose arena holds less than that in free blocks,
* leaves the arena as it is. */
#define SCR_SA_TRIM_MIN ((size_t)16 << 20)
typedef struct {
char *lo, *hi;
} ScrSaRun;
typedef struct {
ScrSaRun *runs; /* zeroed regions left to carve, carved from the end */
size_t n, cap;
char *bump, *lim; /* the frontier while a region is carved (lim != NULL) */
} ScrSaHoles;
static ScrSaHoles scr_sa_holes[SCR_SA_ARENAS][SCR_SA_NCLASS];
/* Bytes carved from the calling thread's slices: up to each frontier. */
static size_t scr_sa_carved(void) {
size_t carved = 0;
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
if (!scr_sa.lim[c]) continue;
const ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
char *top = h->lim ? h->bump : scr_sa.bump[c];
carved += (size_t)(top - ((char *)scr_sa.base + ((size_t)c << scr_sa.shift)));
}
return carved;
}
/* For the slow path of class c, whose bump room is below sz: moves the
* bump pointer to the next zeroed region, or back to the frontier. */
static void scr_sa_next_hole(unsigned c, size_t sz) {
ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
if ((size_t)(scr_sa.lim[c] - scr_sa.bump[c]) >= sz || (!h->n && !h->lim)) return;
if (h->n) {
if (!h->lim) {
h->bump = scr_sa.bump[c];
h->lim = scr_sa.lim[c];
}
ScrSaRun r = h->runs[--h->n];
scr_sa.bump[c] = r.lo;
scr_sa.lim[c] = r.hi;
} else {
scr_sa.bump[c] = h->bump;
scr_sa.lim[c] = h->lim;
h->bump = h->lim = NULL;
}
}
static bool scr_sa_push_run(ScrSaHoles *h, char *lo, char *hi) {
if (h->n == h->cap) {
size_t cap = h->cap ? 2 * h->cap : 64;
ScrSaRun *runs = realloc(h->runs, cap * sizeof *runs);
if (!runs) return false;
h->runs = runs;
h->cap = cap;
}
h->runs[h->n++] = (ScrSaRun){lo, hi};
return true;
}
/* With SCR_SA_STATS=1: carved, free and returned bytes of the parking thread. */
static _Thread_local size_t scr_sa_trim_stats[3];
/* Returns the pages of class c that hold only free blocks (see above). */
static void scr_sa_trim_class(unsigned c, size_t page) {
ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
size_t sz = scr_sa_class_size(c);
char *start = (char *)scr_sa.base + ((size_t)c << scr_sa.shift);
/* Close the region being carved: its rest is still zero. */
if (h->lim) {
char *lo = scr_sa.bump[c], *hi = scr_sa.lim[c];
scr_sa.bump[c] = h->bump;
scr_sa.lim[c] = h->lim;
h->bump = h->lim = NULL;
if ((size_t)(hi - lo) >= sz && !scr_sa_push_run(h, lo, hi)) return;
}
char *top = scr_sa.bump[c];
size_t nblocks = (size_t)(top - start) / sz;
if (!nblocks) return;
size_t words = (nblocks + 63) / 64;
uint64_t *bits = mmap(NULL, words * 8, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (bits == MAP_FAILED) return;
size_t nfree = 0;
for (ScrSaBlock *b = scr_sa.free[c]; b; b = b->next) {
size_t k = (size_t)((char *)b - start) / sz;
bits[k / 64] |= (uint64_t)1 << (k % 64);
nfree++;
}
/* Uncarved zeroed regions count as free; they are rebuilt below. */
for (size_t r = 0; r < h->n; r++) {
for (char *q = h->runs[r].lo; q + sz <= h->runs[r].hi; q += sz) {
size_t k = (size_t)(q - start) / sz;
bits[k / 64] |= (uint64_t)1 << (k % 64);
}
}
scr_sa_trim_stats[0] += nblocks * sz;
scr_sa_trim_stats[1] += nfree * sz;
#define SCR_SA_BIT(k) ((bits[(k) / 64] >> ((k) % 64)) & 1)
/* Whole pages below the frontier whose blocks are all free. */
uintptr_t first = ((uintptr_t)start + page - 1) & ~(uintptr_t)(page - 1);
uintptr_t last = (uintptr_t)top & ~(uintptr_t)(page - 1);
h->n = 0;
char *run = NULL;
for (uintptr_t pg = first; pg <= last; pg += page) {
bool empty = false;
if (pg < last) {
size_t k0 = (size_t)((char *)pg - start) / sz;
size_t k1 = (size_t)((char *)pg + page - 1 - start) / sz;
if (k1 >= nblocks) k1 = nblocks - 1;
empty = true;
for (size_t k = k0; k <= k1 && empty; k++) empty = SCR_SA_BIT(k);
}
if (empty && !run) run = (char *)pg;
if (!empty && run) {
(void)madvise(run, (size_t)((char *)pg - run), MADV_DONTNEED);
scr_sa_trim_stats[2] += (size_t)((char *)pg - run);
/* Blocks wholly inside the returned pages carve from it later. */
char *lo = start + ((size_t)(run - start) + sz - 1) / sz * sz;
char *hi = start + ((size_t)((char *)pg - start) / sz) * sz;
if (hi > lo) (void)scr_sa_push_run(h, lo, hi);
for (char *q = lo; q < hi; q += sz) {
size_t k = (size_t)(q - start) / sz;
bits[k / 64] &= ~((uint64_t)1 << (k % 64));
}
/* A block starting in the returned pages but ending on a kept one is
* dropped: relinking it would fault its page back in. */
if (hi < (char *)pg && hi >= run) {
size_t k = (size_t)(hi - start) / sz;
bits[k / 64] &= ~((uint64_t)1 << (k % 64));
}
run = NULL;
}
}
#undef SCR_SA_BIT
/* The remaining free blocks, in address order. */
ScrSaBlock *head = NULL, **tail = &head;
for (size_t w = 0; w < words; w++) {
for (uint64_t m = bits[w]; m; m &= m - 1) {
size_t k = w * 64 + (size_t)__builtin_ctzll(m);
ScrSaBlock *b = (ScrSaBlock *)(start + k * sz);
*tail = b;
tail = &b->next;
}
}
*tail = NULL;
scr_sa.free[c] = head;
munmap(bits, words * 8);
/* The next owner carves the returned regions before the frontier. */
if (h->n) {
h->bump = scr_sa.bump[c];
h->lim = scr_sa.lim[c];
ScrSaRun r = h->runs[--h->n];
scr_sa.bump[c] = r.lo;
scr_sa.lim[c] = r.hi;
}
}
/* Whether the calling thread carved at least SCR_SA_TRIM_MIN since it
* claimed its arena: a short-lived thread reusing the arena's free blocks
* leaves them for the next one. */
static bool scr_sa_trim_worth(void) {
return scr_sa_carved() - scr_sa_claim_carved[scr_sa_arena] >= SCR_SA_TRIM_MIN;
}
/* Returns the free pages of the calling thread's arena (see above). */
static void scr_sa_trim(void) {
scr_sa_drain_remote();
size_t free_bytes = 0;
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
size_t n = 0, sz = scr_sa_class_size(c);
for (ScrSaBlock *b = scr_sa.free[c]; b && n * sz < SCR_SA_TRIM_MIN; b = b->next) n++;
free_bytes += n * sz;
if (free_bytes >= SCR_SA_TRIM_MIN) break;
}
const char *stats = getenv("SCR_SA_STATS");
const char *off = getenv("SCR_SA_TRIM");
bool trim = free_bytes >= SCR_SA_TRIM_MIN && !(off && off[0] == '0');
memset(scr_sa_trim_stats, 0, sizeof scr_sa_trim_stats);
if (!trim && stats && stats[0] == '1')
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
if (!scr_sa.lim[c]) continue;
size_t sz = scr_sa_class_size(c);
char *start = (char *)scr_sa.base + ((size_t)c << scr_sa.shift);
scr_sa_trim_stats[0] += (size_t)(scr_sa.bump[c] - start);
for (ScrSaBlock *b = scr_sa.free[c]; b; b = b->next) scr_sa_trim_stats[1] += sz;
}
if (trim) {
long page = sysconf(_SC_PAGESIZE);
for (unsigned c = 0; c < SCR_SA_NCLASS; c++)
if (scr_sa.lim[c]) scr_sa_trim_class(c, page > 0 ? (size_t)page : 4096);
}
#if defined(__GLIBC__)
/* Large blocks: the system allocator's free pages too. */
if (trim) (void)malloc_trim(0);
#endif
if (stats && stats[0] == '1')
fprintf(stderr, "scr_sa: park arena %d trim %d carved %zu MB free %zu MB returned %zu MB\n",
scr_sa_arena, (int)trim, scr_sa_trim_stats[0] >> 20, scr_sa_trim_stats[1] >> 20,
scr_sa_trim_stats[2] >> 20);
}
#endif
#ifdef SCR_SA_TRIM
/* Trims run on a detached helper thread: the parent of a worker joins it
* when it handles the worker's exit, which must not wait for the trim (at
* the end of a program every worker exits at once, and the process ends
* right after). The helper takes over the exiting thread's claim on the
* arena, so no thread claims it during the trim. */
static _Atomic(unsigned) scr_sa_trimming; /* helpers running */
static void scr_sa_release(unsigned i) {
scr_sa_parked[i] = scr_sa;
memset(&scr_sa, 0, sizeof scr_sa);
scr_sa_arena = -1;
scr_sa_refused = true;
atomic_fetch_and_explicit(&scr_sa_claimed, ~(1u << i), memory_order_release);
}
static void *scr_sa_trim_main(void *arg) {
unsigned i = (unsigned)(uintptr_t)arg;
scr_sa_arena = (int)i;
scr_sa = scr_sa_parked[i];
scr_sa_trim();
scr_sa_release(i);
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_release);
return NULL;
}
void scr_sa_wait_trims(void) {
while (atomic_load_explicit(&scr_sa_trimming, memory_order_acquire)) {
struct timespec pause = {0, 1000000};
nanosleep(&pause, NULL);
}
}
#else
void scr_sa_wait_trims(void) {}
#endif
void scr_sa_thread_park(void) {
if (scr_sa_arena < 0) return;
unsigned i = (unsigned)scr_sa_arena;
#ifdef SCR_SA_TRIM
if (scr_sa_trim_worth()) {
scr_sa_parked[i] = scr_sa;
atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
pthread_attr_t attr;
pthread_t helper;
bool started = pthread_attr_init(&attr) == 0;
if (started) {
started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 &&
pthread_create(&helper, &attr, scr_sa_trim_main, (void *)(uintptr_t)i) == 0;
pthread_attr_destroy(&attr);
}
if (started) {
/* Later frees on this thread take the foreign path into the arena's
* remote list; the helper releases the claim. */
memset(&scr_sa, 0, sizeof scr_sa);
scr_sa_arena = -1;
scr_sa_refused = true;
return;
}
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
scr_sa_trim();
}
#endif
scr_sa_parked[i] = scr_sa;
/* Later frees on this thread (thread-exit destructors) take the foreign
* path into the parked arena's remote list. */
memset(&scr_sa, 0, sizeof scr_sa);
scr_sa_arena = -1;
scr_sa_refused = true;
atomic_fetch_and_explicit(&scr_sa_claimed, ~(1u << i), memory_order_release);
}
/* The class size of a block in another thread's arena, or 0 outside the
* region (a system block). */
static size_t scr_sa_foreign_size(const void *p) {
uintptr_t region = atomic_load_explicit(&scr_sa_region, memory_order_acquire);
uintptr_t off = (uintptr_t)p - region;
if (!region || off >= scr_sa_arena_span * SCR_SA_ARENAS) return 0;
return scr_sa_class_size((unsigned)((off % scr_sa_arena_span) >> scr_sa_region_shift));
}
void scr_sa_free_foreign(void *p) {
uintptr_t region = atomic_load_explicit(&scr_sa_region, memory_order_acquire);
uintptr_t off = (uintptr_t)p - region;
if (region && off < scr_sa_arena_span * SCR_SA_ARENAS) {
unsigned i = (unsigned)(off / scr_sa_arena_span);
ScrSaBlock *b = p;
ScrSaBlock *head = atomic_load_explicit(&scr_sa_remote[i], memory_order_relaxed);
do b->next = head;
while (!atomic_compare_exchange_weak_explicit(&scr_sa_remote[i], &head, b, memory_order_release,
memory_order_relaxed));
return;
}
free(p);
}
#else
static bool scr_sa_refused;
/* Reserve address space for every class slice at once; smaller slices are
* tried when the host limits address space or overcommit. */
static bool scr_sa_reserve(void) {
for (unsigned shift = 30; shift >= 24; shift -= 2) {
size_t span = (size_t)SCR_SA_NCLASS << shift;
void *p = scr_sa_map(span);
if (!p) continue;
scr_sa.base = (uintptr_t)p;
scr_sa.shift = shift;
scr_sa.span = span;
return true;
}
scr_sa_refused = true;
return false;
}
#endif /* SCR_WORKERS */
/* Runs once per class (to reserve and open its slice) and then only for
* blocks the fast path does not serve. */
__attribute__((cold, noinline)) void *scr_sa_slow(size_t n, bool zero) {
if (n - 1 < SCR_SA_MAX && (scr_sa.span || (!scr_sa_refused && scr_sa_reserve()))) {
unsigned c = scr_sa_class(n);
#ifdef SCR_WORKERS
/* A claimed arena may be a parked one with free blocks and bump room,
* and other threads may have returned blocks since the last miss. */
if (atomic_load_explicit(&scr_sa_remote[scr_sa_arena], memory_order_relaxed))
scr_sa_drain_remote();
ScrSaBlock *b = scr_sa.free[c];
if (b) {
scr_sa.free[c] = b->next;
if (zero) memset(b, 0, n);
return b;
}
#endif
/* Carve from the slice, opening it or raising its limit as needed;
* fresh pages are zero. */
size_t sz = scr_sa_class_size(c);
#ifdef SCR_SA_TRIM
if (scr_sa.lim[c]) scr_sa_next_hole(c, sz);
#endif
char *start = (char *)scr_sa.base + ((size_t)c << scr_sa.shift);
char *end = start + ((size_t)1 << scr_sa.shift);
char *p = scr_sa.lim[c] ? scr_sa.bump[c] : start;
char *lim = scr_sa.lim[c] ? scr_sa.lim[c] : start;
#ifdef SCR_SA_DUMP
char *from = lim;
#endif
while ((size_t)(lim - p) < sz && lim < end) lim = scr_sa_extend(start, lim, end);
#ifdef SCR_SA_DUMP
if (lim > from) scr_sa_dumpable(start, end, from, lim);
#endif
scr_sa.lim[c] = lim;
if ((size_t)(lim - p) >= sz) {
scr_sa.bump[c] = p + sz;
return p;
}
scr_sa.bump[c] = p;
}
/* Large or zero-sized, an exhausted slice, or no reservation. */
void *q = zero ? calloc(1, n) : malloc(n);
if (n > SCR_SA_MAX) SCR_SA_LARGE_NOTE(q, n);
return q;
}
void *scr_mem_realloc(void *p, size_t n) {
if (!p) return scr_mem_alloc(n);
uintptr_t off = (uintptr_t)p - scr_sa.base;
size_t have;
if (off < scr_sa.span) have = scr_sa_class_size((unsigned)(off >> scr_sa.shift));
#ifdef SCR_WORKERS
else if ((have = scr_sa_foreign_size(p)) != 0) {
/* Another arena's block: move it into this thread's allocator. */
}
#endif
else {
void *q = realloc(p, n);
if (n > SCR_SA_MAX) SCR_SA_LARGE_NOTE(q, n);
return q;
}
/* Still fits its block, which is aligned for n (see the contract). */
if (n && n <= have && (n % 16 || have % 16 == 0)) return p;
void *q = scr_mem_alloc(n ? n : 1);
if (!q) return NULL;
memcpy(q, p, have < n ? have : n);
scr_mem_free(p);
return q;
}
#else
void *scr_mem_realloc(void *p, size_t n) { return realloc(p, n); }
#ifdef SCR_WORKERS
void scr_sa_thread_park(void) {}
void scr_sa_wait_trims(void) {}
void scr_sa_free_foreign(void *p) { free(p); }
#endif
#endif
void *scr_rt_calloc(size_t n) { return scr_mem_calloc(n); }
void scr_rt_free(void *p) { scr_mem_free(p); }
void scr_mem_prefault(void *lo, void *hi) {
#if SCR_SMALL_ALLOC && defined(SCR_SA_PREFAULT)
scr_sa_prefault(lo, hi);
#else
(void)lo;
(void)hi;
#endif
}
#if defined(__SANITIZE_ADDRESS__)
#define SCR_HEAP_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#define SCR_HEAP_SANITIZER 1
#endif
#endif
#if defined(SCR_HEAP_SANITIZER)
#include <sanitizer/allocator_interface.h>
#elif defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 33))
#include <malloc.h>
#define SCR_HEAP_MALLINFO2 1
#elif defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(_WIN32)
#include <malloc.h>
#include <windows.h>
#endif
/* process.memoryUsage's heap figures: the calling thread's small-object
* slices (the bytes carved from them; in use is that less the blocks on
* their free lists and the zeroed holes left to carve) plus the system
* allocator's own totals where the platform reports them (glibc's
* 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. */
void scr_heap_stats(double *total, double *used) {
size_t held = 0, idle = 0;
#if SCR_SMALL_ALLOC
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
if (!scr_sa.lim[c]) continue;
size_t sz = scr_sa_class_size(c);
char *start = (char *)scr_sa.base + ((size_t)c << scr_sa.shift);
char *top = scr_sa.bump[c];
#ifdef SCR_SA_TRIM
const ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
if (h->lim) {
top = h->bump;
idle += (size_t)(scr_sa.lim[c] - scr_sa.bump[c]);
}
for (size_t i = 0; i < h->n; i++) idle += (size_t)(h->runs[i].hi - h->runs[i].lo);
#endif
held += (size_t)(top - start);
for (const ScrSaBlock *b = scr_sa.free[c]; b; b = b->next) idle += sz;
}
#endif
size_t sys_held = 0, sys_used = 0;
#if defined(SCR_HEAP_SANITIZER)
sys_held = __sanitizer_get_heap_size();
sys_used = __sanitizer_get_current_allocated_bytes();
#elif defined(SCR_HEAP_MALLINFO2)
struct mallinfo2 info = mallinfo2();
sys_held = info.arena + info.hblkhd;
sys_used = info.uordblks + info.hblkhd;
#elif defined(__APPLE__)
malloc_statistics_t stats;
malloc_zone_statistics(NULL, &stats);
sys_held = stats.size_allocated;
sys_used = stats.size_in_use;
#elif defined(_WIN32)
typedef struct {
DWORD cb;
SIZE_T allocated;
SIZE_T committed;
SIZE_T reserved;
SIZE_T max_reserve;
} ScrHeapSummary;
typedef BOOL(WINAPI * SummaryFn)(HANDLE, DWORD, ScrHeapSummary *);
HMODULE kernel = GetModuleHandleA("kernel32.dll");
SummaryFn summary = kernel ? (SummaryFn)(void *)GetProcAddress(kernel, "HeapSummary") : NULL;
ScrHeapSummary s = {sizeof s, 0, 0, 0, 0};
if (summary && summary((HANDLE)_get_heap_handle(), 0, &s)) {
sys_held = s.committed;
sys_used = s.allocated;
}
#endif
*total = (double)(held + sys_held);
*used = (double)(held - (idle < held ? idle : held) + sys_used);
}