551 Commits
Author SHA1 Message Date
Chris Tate ef25cf119d Prepare v0.3.0 2026-10-12 15:57:06 -05:00
Chris Tate bbf2c34778 process.constrainedMemory() and availableMemory() read the process's own cgroup, like libuv (#893) 2026-10-12 14:21:30 -05:00
Chris Tate cca079d705 Find multi-byte needles without a memchr call per first-byte hit (#892) 2026-10-12 14:20:36 -05:00
Chris Tate 8c035be7de Merge main into fix/constrained-memory-libuv 2026-10-12 13:18:23 -05:00
Chris Tate 8c4bb3277c Merge main into perf/string-search-pairs 2026-10-12 13:16:39 -05:00
Chris Tate 829a326bb7 Merge main into perf/string-maps 2026-10-12 13:14:33 -05:00
Chris Tate 7e6294ca0f Make scriptc compilation native-only 2026-10-12 12:25:30 -05:00
Chris Tate d75d7a22f0 Prepare v0.2.10 2026-10-12 10:31:02 -05:00
Chris Tate 043c656bbd Merge main into feat/program-lifetime-publication 2026-10-12 09:38:24 -05:00
Malte Ubl 09118a3c25 Read the process's own cgroup in process.constrainedMemory() like libuv
scr_constrained_memory read /sys/fs/cgroup/memory.max, the root of the cgroup mount, and answered
0 for "max". Node's process.constrainedMemory() is libuv's uv_get_constrained_memory (libuv 1.51
in Node 24.15): the cgroup named by /proc/self/cgroup, the smaller of memory.max and memory.high
(v2) or of memory.limit_in_bytes and memory.soft_limit_in_bytes (v1, falling back to the
controller's root), with "max" and v1's page-aligned LONG_MAX read as UINT64_MAX and an unreadable
file making the answer 0. The two only agreed inside a cgroup namespace; in a nested cgroup or a
systemd scope the native build reported the root's limit (17179869184 in a 16 GB sandbox where Node
answers 18446744073709552000, or 2147483648 under memory.high=2G), so programs that size
themselves by it ran out of memory.

process.availableMemory() follows uv_get_available_memory too: under a constraint no larger than the
machine's memory, the constraint minus the cgroup's current usage; otherwise MemAvailable, then
sysinfo's freeram.

The corpus program prints the exact constraint, so Node and the native build must read the same
cgroup.
2026-10-12 07:22:54 -07:00
Malte Ubl 5201ab9340 Find multi-byte needles without a memchr call per first-byte hit
scr_byte_find called memchr for the needle's first byte and memcmp at
every hit. Path searches such as indexOf('/./') or indexOf('//') hit the
first byte at every separator, so each search paid two library calls per
path segment.

Haystacks of up to 256 bytes now test eight candidate starts per step for
the first two needle bytes with a SWAR zero-byte test (no false negatives;
false positives are rejected by the byte checks) and compare the rest only
at real two-byte matches. Longer haystacks keep memchr but check the
second byte inline before memcmp.

The tsc-ts parser (tRPC, parse only) runs 1.1% fewer instructions; string
search there drops from 111M to 82M instructions.

The new corpus program covers both strategies, every match offset around
the 8-byte steps and the 256-byte cut, near misses, the 0x01 false
positive pattern, and non-ASCII needles and haystacks.
2026-10-12 07:22:54 -07:00
Malte Ubl b0fec9e55c Find string map keys without a stack frame
scr_map_entry_str serves every string-key Map/Set read of generated code.
For a key with a cached hash in linear or compact-bucket storage it is now
a leaf: the filter test, the probe and the identity comparison run without
saving registers. A hash match on another string object tail-calls the
byte comparison; uncached hashes and wide tables take the complete lookup.
2026-10-12 07:22:52 -07:00
Malte Ubl 726dc34971 Keep the map filter without extra passes
Growth rebuilds the bucket table from unchanged entries, so the filter it
already has stays valid; only compaction drops entries, and it now
recomputes the filter in its copy loop instead of a second pass.
2026-10-12 07:22:52 -07:00
Malte Ubl f04681f0b6 Reject absent map keys through a filter in the map header
Every live entry's hash sets two bits of a new 64-bit ScrMap.filter, so a
lookup whose bits are not both set is absent without loading the bucket
table or the entries. String-keyed lookups in tsc-ts are mostly misses
(Map.get 67% on Excalidraw, has 87%), and the filter rejects 84-88% of
the misses on non-empty maps. Deletions leave their bits; rebuilds,
compaction and clear recompute the filter from the live entries.

Lookups also return the entry index straight from the probe instead of
reading the bucket again. The filter sits after nlive, so the backend's
%ScrMapIx fields for entries and the direct index move by one.
2026-10-12 07:22:52 -07:00
Malte Ubl e1ccfeeb84 codegen: signed conversions for array counters and lengths
Integer induction counters live in unsigned 64-bit slots, so every read
as a number was a uitofp i64 (a 6-7 instruction sequence on x86-64) and
every nested start an fptoui. The runtime's push path also round-tripped
the array length through a double for its bound check.

The values are at most 2^32 - 1: on 64-bit targets convert counters with
sitofp and loop starts with fptosi, convert array lengths and validated
indices through int64_t in scr_array.c, and test the push bound on the
integer length. 32-bit targets keep the unsigned conversions.

tsc-ts Excalidraw: uitofp i64 sequences 32.5 M -> 4.0 M, -0.66%
instructions on top of the throw-path change.
2026-10-12 06:40:50 -07:00
Malte Ubl b5e4bcb54e runtime: keep throw entry points cold and out of line
With runtime bitcode import, scr_throw_obj, scr_error_new and the other
throw entry points were inlined into the error paths of small compiled
functions. The inlined bodies needed callee-saved registers that every
call then saved and restored, and they made the helpers too large to
inline into their callers (1047 inlined throw bodies in tsc-ts).

Mark the 25 functions that only run while raising a JS exception
cold and noinline. The LLVM helper never imports noinline bodies, so
the throw paths stay calls. tsc-ts Excalidraw: -0.44% instructions.
2026-10-12 06:40:50 -07:00
Chris Tate 85736170ae Merge main into feat/program-lifetime-publication 2026-10-12 08:13:15 -05:00
Chris Tate 72cc828ab0 Merge main into perf/compact-array-header-2 2026-10-12 01:12:21 -05:00
Chris Tate b1f3b31546 Order the heap page size before huge page advice 2026-10-11 22:58:10 -05:00
Malte Ubl fa55ce6b07 Keep the RC entry points of array elements in one shared descriptor
Every SCR_ELEM_REF array stored its element type's retain, release and trace entry points in its own header (24 bytes), so ScrArr was 80 bytes: an 80-byte block for arrays of acyclic elements and a 96-byte block for cycle-capable ones. The three pointers now live in one constant ScrArrOps descriptor per element type, which the compiler emits once per module (@sc_arrops.*) and the runtime keeps static, and the header holds a pointer to it plus a 4-byte `cyc` flag beside `elem` that the inline RC fast paths test. ScrArr is 64 bytes: arrays take 64- and 80-byte blocks.

scr_arr_new_ref(retain, release, trace, cap) becomes scr_arr_new_ops(ops, cap); scr_set_to_arr_ref takes the result's descriptor from its caller instead of building an array from the set's key adapters. The backend names the %ScrArr fields in SCR_ARR_FIELD. The layout change bumps the runtime ABI marker to scr_runtime_abi_v13.
2026-10-11 19:46:48 -07:00
Chris Tate 31aaefb4c6 Merge main into perf/map-get-inline-retain 2026-10-11 21:31:11 -05:00
Chris Tate 6b708b2e3e Merge pull request #876 from vercel-labs/perf/array-growth
Size small arrays exactly and keep map storage on the runtime allocator
2026-10-11 20:14:17 -05:00
Malte Ubl 40a2c2adf2 ProgramLifetime in sanitized builds: skip the RC audit's leak report and scan instances as LeakSanitizer roots
Program-lifetime instances are never freed, and neither is anything they hold, so the RC audit of --sanitize builds reported every one of them, and every string, array and map they reference, as live at exit (4633-contract-program-lifetime failed the sanitized differential lane: 306 strings, 15 arrays, 338 objects). Like a program that abandons fibers, a program that keeps program-lifetime objects now skips the leak report with "scriptc RC audit skipped: N program-lifetime object(s) never freed", which the harnesses already filter.

Sanitized constructors call scr_obj_lifetime_note(obj), which counts the instance and registers it with LeakSanitizer as an intentional root, as publication does for published objects: once the audit no longer exits early, LeakSanitizer would otherwise report the instances the program drops. Plain builds emit nothing.
2026-10-11 17:07:03 -07:00
Malte Ubl 5af0c17c39 Merge branch 'main' into perf/array-growth 2026-10-11 16:32:10 -07:00
Malte Ubl 825ff1dcfe Merge branch 'main' into perf/locality 2026-10-11 16:32:06 -07:00
Malte Ubl 7027039809 Merge branch 'main' into perf/map-get-inline-retain 2026-10-11 16:31:53 -07:00
Malte Ubl cdf964ea85 Allocate plain map headers out of line
Map construction inlines into compiled callers, and the inline small-allocator path (with its clearing of recycled blocks) added 37 KB to tsc-ts across its constructors and init functions. A noinline helper keeps those call sites as small as the libc calloc call they had: tsc-ts text is now +5.6 KB over origin/main for this branch.
2026-10-11 16:30:23 -07:00
Malte Ubl 2a7c22ae49 Keep map storage on the runtime allocator
Acyclic map headers, entry arrays, bucket tables and number-key indices came from glibc (calloc/malloc/realloc/free) even when small: a map with one entry paid a 160-byte glibc header plus a 96-byte glibc entry block, and every growth step went through glibc realloc. They now use scr_mem_calloc/alloc/realloc/free like arrays, so blocks up to 512 bytes come from the size-class slices (inline free lists, huge pages after the slice prefix) and larger ones still go to the system through scr_mem_realloc. Nothing outside scr_map.c allocates or frees map storage.
2026-10-11 16:30:22 -07:00
Malte Ubl 6778826d8f Keep the array growth sequence; round only exact construction to its block
Rounding every dense capacity up to its allocator block changed the growth sequence (a 48-byte block holds 5 slots, so storage went 48/96/192/384 B instead of 48/80/144/288/576 B), which moved array storage into other size classes: +3.6% simulated L1 misses on the tsc-ts self-check and +2.6%/+4.3% wall on the self-check and TypeORM. Growth is back to 4, 8, 16, ...; only exactly sized construction fills its block. Adds a runtime test for exact construction followed by appends, index writes and length extension over the spare capacity.
2026-10-11 16:30:22 -07:00
Malte Ubl ead9f4ecba Size array storage to its construction count and its allocator block
Arrays constructed with a known element count (literals, slices, map/filter results) reserved at least four slots, so a one-element literal took a 48-byte block for 9 bytes of content. Construction now allocates exactly the requested slots, and any dense capacity is rounded up to fill its small-object allocator block (1 slot -> 16 B, 2-3 -> 32 B). Appends below four slots grow straight to four and double from there as before. The first storage of an array is a plain allocation with its states cleared, without the realloc, state move and sparse absorption of later growth. New capacity still starts as holes.
2026-10-11 16:30:21 -07:00
Chris Tate c9fe46d8c0 Merge main into perf/offthread-prefault 2026-10-11 18:10:02 -05:00
Chris Tate fa11424e5f Prepare v0.2.9 2026-10-11 17:25:14 -05:00
Malte Ubl eae8e02481 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:25 -07:00
Malte Ubl 0f05bf368d Pre-fault only for large slices and heaps
The first version committed memory ahead for every program: the heap's
16 MiB top pad was faulted in as soon as the helper started, and every
slice past its first 256 KiB faulted the next chunk ahead. A small server
(runtime suite http-api) peaked at 20.7 MiB instead of 4.4 MiB.

- The helper now only watches the break until the heap has grown by
  32 MiB; only then does it set the top pad and fault growth in ahead.
  Smaller heaps keep glibc's defaults, including its dynamic mmap
  threshold, which any top-pad mallopt fixes.
- A slice faults ahead only once it has carved 1 MiB, so lightly used
  classes commit nothing extra (and a program that never fills one does
  not start the helper at all).
2026-10-11 13:07:04 -07:00
Malte Ubl 767c33ebdb Give glibc's large blocks transparent huge pages
Blocks above the small-object classes (array storage, hash tables, link pages, large strings) come from glibc, whose brk heap and per-block mappings stay on 4 KiB pages where THP is in madvise mode; glibc advises its own memory only under the glibc.malloc.hugetlb tunable, which a program cannot set for itself. Once the brk heap passes 32 MiB, every large allocation now advises the heap grown since the previous one (and the aligned interior of big blocks mapped outside it), and glibc's top pad is raised to 32 MiB so heap growth no longer leaves each 2 MiB range straddling the heap end when it is first touched; the mmap and trim thresholds are pinned to their adaptive maxima, which setting the pad would otherwise freeze at their defaults. Linux and glibc only.
2026-10-11 12:58:50 -07:00
Malte Ubl 2278456bbd Pre-fault allocator memory on a helper thread (Linux)
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).
2026-10-11 12:38:24 -07:00
Malte Ubl fc93d10871 Merge remote-tracking branch 'origin/perf/array-layout-main' into HEAD 2026-10-11 12:23:05 -07:00
Malte Ubl cc8392d26a Merge remote-tracking branch 'origin/perf/heap-layout-main' into HEAD 2026-10-11 12:22:56 -07:00
Malte Ubl c8430bbf0d Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 12:22:49 -07:00
Malte Ubl e84adc9727 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 12:22:40 -07:00
Chris Tate baeaf9c7e0 Merge pull request #854 from vercel-labs/perf/exception-flag-main
Exact per-thread pending word for worker executables, and a call-free macOS stack guard
2026-10-11 14:06:38 -05:00
Chris Tate 0ab65a79b0 Merge pull request #836 from vercel-labs/perf/parmem
Support os.availableParallelism() in static programs
2026-10-11 12:55:51 -05:00
Malte Ubl 00f88680cd Merge remote-tracking branch 'origin/main' into HEAD 2026-10-11 10:17:26 -07:00
Malte Ubl 112add3dce Merge remote-tracking branch 'origin/perf/gc-trace-null-skip-main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 10:17:20 -07:00
Malte Ubl 796013f9d2 Merge remote-tracking branch 'origin/perf/heap-layout-main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 10:17:07 -07:00
Malte Ubl fca66ec655 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 10:16:59 -07:00
Malte Ubl d770871069 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 10:16:52 -07:00
Malte Ubl a34d018497 Merge remote-tracking branch 'origin/perf/exception-checks-main' into HEAD 2026-10-11 10:16:45 -07:00
Malte Ubl fe79aea42e Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 10:15:10 -07:00
Chris Tate 2db40a34e8 Merge pull request #859 from vercel-labs/perf/worker-fd-table
Grow the descriptor table before worker threads share it
2026-10-11 12:07:56 -05:00