First touch of fresh memory costs the allocating thread a page fault and
a page clear. Linux executables now hand that to a helper thread:
- Size-class slices raise their bump limits in steps (256 KiB on the
small-page prefix, half a huge page after the switch), and each step
asks the helper to fault in the next step ahead with
MADV_POPULATE_WRITE, which commits zeroed pages without storing, so it
cannot race with carving. A slice holds at most about one step of
untouched committed memory; a slice's first chunk is not faulted ahead.
- glibc's heap gets a 16 MiB top pad and the helper faults in each brk
extension as soon as it sees the break move (checked after every
request, then polled every 1 ms while it keeps growing; an idle program
does not wake it).
The fast paths are unchanged; pages the helper has not reached are still
faulted by the owner. The helper masks all signals. SCR_SA_PREFAULT=0
turns it off, SCR_SA_PREFAULT_HEAP=0 keeps glibc's heap defaults; kernels
without MADV_POPULATE_WRITE (< 5.14) fall back to the old policy, and
library archives keep it.
The slice-growth allocator check now carves past several huge-page steps
and checks every fresh block reads as zero (UBSan and TSan builds run
with the helper).
Counting array appends as edge-preserving makes `keep(item, items) { items.push(item); return item; }` a borrowed-return function: it preserves every heap edge, borrows every reference parameter and returns one of them. Its borrowing body now retains only the appended element and returns the parameter +0, and the owned adapter retains the result before releasing its parameters. An append never releases an element (the slow path stores past the old length), so the caller's owner keeps the result alive.
These PRs were merged into their stack base branches instead of main. This merges perf/alloc-hugepages (with the CI fixes below) into main. Conflicts: order-parity.json (union of entries); scr_alloc.c/scr_alloc.test.ts, where main already has the worker exit trim (#858, adapted to the pre-hugepages allocator): resolved by taking the hugepages allocator and re-applying the original trim commits written on top of it (ea88b87c + 4816696d, clean apply on an identical base).
Brings in cc7c6cb7 (inline truncation decided for the host when no triple is given), which this branch predates; without it the native bootstrap's LLVM comparison with the Node seed fails on x86-64 Linux.
os.availableParallelism() follows libuv's uv_available_parallelism, whose uv__slurp reads each /proc and cgroup file with a single read and fails on a read error; the helper looped until the buffer filled and ignored read errors, so a long /proc/self/cgroup could resolve a controller Node does not see. A cgroup CPU quota below one CPU truncates to 0 and stays ignored rather than clamped to 1: libuv 1.51 (Node 24) through 1.52 applies a quota only when it is positive, and Node then answers the affinity count.
Conflicts, all resolved to this branch's side:
- emitter.ts (numeric-table get, map index probe), expr-bytes.ts, local-array-reads.ts, string-reads.ts: the base only respelled the size-to-double conversions in the old range checks (uitofp nneg, now sitofp); exactIndexLines replaces those checks entirely. The base's other hunks in these files (host inline truncation, the remaining sitofp conversions) merge as they are.
- int32-slots.ts: #837 and #843 both extracted dynamicFieldStores with identical bodies; keep this branch's signature, whose optional records/unions maps also serve #837's single-argument callers.
The `nneg` flag on `uitofp` is LLVM 19 syntax. Sanitized builds hand the textual module to the host clang, and Ubuntu 24.04's clang 18 rejects it ("expected type"), which failed every Linux sanitized Effect shard. These operands are size_t values below 2^63, so `sitofp` converts them identically, is accepted by every supported LLVM, and still lowers to one cvtsi2sd on x86-64 (LLVM 19+ canonicalizes it to `uitofp nneg` where it can prove the sign).
The emitter defines @scr_detached_exit for --detached-exit executables, and the runtime ABI test requires every scr_* symbol the backend can emit to have a prototype or extern data declaration in the header.
scr_runtime.h declares uintptr_t scr_stack_guard_get(void), an i64 in the C ABI, but the Darwin worker stack check's cold fallback declared and called it returning ptr, which the runtime ABI test rejects. Declare i64 and convert the result with inttoptr.
Since #837 a type is traced only if it lies in a cycle-capable component or reaches an intrinsic reference; a type that merely holds a cycle cannot be reached back from it. In both backend-analysis cases `record:outer` only holds `record:cycle`, so the Node oracle and the native compiler agree on ["record:cycle"] and the expectations still listed `record:outer`.
Brings in cc7c6cb7 (inline truncation decided for the host when no triple is given), which this branch predates; without it the native bootstrap's LLVM comparison with the Node seed fails on x86-64 Linux.
The custom Dockerfile.sandbox image builds the LLVM helper against the same LLVM 22 CMake exports as the managed bootstrap, which reference the Polly libraries; without libpolly-22-dev CMake configure fails.
Three workers each hold and drop about 80 MB of small objects and exit;
three more then allocate as much again in the arenas the first ones
returned (on Linux the first ones hand those pages back when they exit).
Records the fixture's preflight/order baseline.
A script thread that exits parks its allocator arena with every freed
block still resident, so the memory of a finished worker stays with the
process until another thread claims the arena. On Linux, a parking thread
that carved at least 16 MiB now hands back every page of its arena that
holds only free blocks (MADV_DONTNEED), relinks the remaining free blocks
in address order, and keeps the returned page runs as zeroed regions that
the next owner carves before the slice frontier. It also trims glibc
malloc. SCR_SA_STATS=1 reports what each park returned; SCR_SA_TRIM=0
turns it off.
`scriptc build|run --detached-exit` opts an executable into handing the address-space teardown after process.exit to a helper process on Linux. The helper shares the address space (CLONE_VM), blocks signals, closes every descriptor but the read end of a pipe and waits for EOF on it. The kernel releases an exiting process's memory (exit_mm) before its files (exit_files), so EOF means the helper holds the last reference; it then exits and the kernel frees the pages there instead of before the parent sees the exit (30-40 ms per GB of 4 KiB pages; tsc-ts 20-110 ms). The same latency mold hides with --fork.
It is a build-time choice, not a run-time knob, because the helper keeps the executable mapped (overwriting the binary right after it exits can fail with ETXTBSY) and the memory charged to the cgroup for up to ~100 ms, and the teardown CPU time leaves the parent's rusage. The program object defines `scr_detached_exit = 1`, overriding the runtime's weak default of 0, so no runtime flavor or linker flag is added. The option is part of the early executable cache and route keys; executables only, no effect on other platforms.
Every worker mailbox is a pipe. Once a process passes 64 descriptors the kernel grows its descriptor table, and while other threads share the table expand_fdtable first waits a full RCU grace period. tsc-ts on a 16-vCPU host stalled 11-25 ms on the thread starting its 14th checker (16 parse threads plus 13 checkers), on the critical path. Worker builds now grow the table to 256 entries at load, while the process has one thread, which never waits; the table never shrinks.