Commit Graph
1329 Commits
Author SHA1 Message Date
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
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
Chris Tate 727bdb7227 Merge pull request #849 from vercel-labs/perf/gc-widen-faster
Widen the old level's growth fraction two doublings at a time
2026-10-11 12:07:34 -05:00
Chris Tate e503d26b17 Merge pull request #842 from vercel-labs/perf/exact-index-checks
Cheaper checks for unproven typed-array and string indexes
2026-10-11 12:06:42 -05:00
Chris Tate c0cedd769c Merge pull request #840 from vercel-labs/perf/borrow-optional-chain-receivers
Borrow the bound receiver of optional chains
2026-10-11 12:06:17 -05:00
Chris Tate 12da5538f7 Merge pull request #834 from vercel-labs/perf/live-typed-ref-access
Access live array references one element at a time
2026-10-11 12:05:44 -05:00
Malte Ubl 4a513b9fd7 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 09:37:04 -07:00
Malte Ubl 69aedab4f8 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-11 09:36:47 -07:00
Malte Ubl d56348effd Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 09:36:22 -07:00
Malte Ubl 98dc0f1314 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-11 09:36:04 -07:00
Malte Ubl e52c7dbbbd Merge remote-tracking branch 'origin/perf/fold-constant-globals' into HEAD 2026-10-11 09:36:03 -07:00
Malte Ubl c4cb109c38 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 09:35:55 -07:00
Malte Ubl 9ae2acdd39 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-11 09:35:37 -07:00
Chris Tate 6cc426c341 Merge pull request #863 from vercel-labs/land/approved-stack
Land the approved performance stack (#829, #830, #832, #833, #835, #837, #839)
2026-10-11 11:32:15 -05:00
Malte Ubl e88e2adb17 Expect a borrowed result from an appending identity callee
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.
2026-10-11 09:11:47 -07:00
Malte Ubl 5c3f6f80e0 Land the approved performance stack (#829, #830, #832, #833, #835, #837, #839)
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).
2026-10-11 08:57:42 -07:00
Malte Ubl a7e91f86ac Record the preflight/order baselines for exact-index-round-trip and final-field-borrows 2026-10-11 08:55:59 -07:00
Chris Tate b2283af904 Merge pull request #831 from vercel-labs/fix/sandbox-bootstrap-polly
Install Polly in the managed sandbox bootstrap
2026-10-11 10:52:14 -05:00
Chris Tate c6822dbbf6 Merge pull request #858 from vercel-labs/perf/worker-exit-trim
runtime: return an exiting worker thread's free pages to the system
2026-10-11 10:51:46 -05:00
Malte Ubl cb9de5c1f9 Merge the fixed perf/cycle-growth-cap into perf/gc-widen-faster 2026-10-11 08:28:18 -07:00
Malte Ubl 39c6a4a033 Merge perf/typed-array-reads into perf/cycle-growth-cap
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.
2026-10-11 08:27:58 -07:00
Malte Ubl 81f8502387 Merge the fixed perf/alloc-hugepages into perf/exact-index-checks
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.
2026-10-11 08:25:48 -07:00
Malte Ubl fac59cfeea Merge the clang 18 size-conversion fix from perf/length-to-double-nneg 2026-10-11 08:14:06 -07:00
Malte Ubl 2077bc9ee6 Spell non-negative size conversions as sitofp for clang 18
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).
2026-10-11 08:13:58 -07:00
Malte Ubl fb291122f5 Expect only cycle members to be traced in the self-hosting analysis cases
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`.
2026-10-11 08:13:08 -07:00
Malte Ubl 5ed8842d43 Merge perf/typed-array-reads into perf/alloc-hugepages
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.
2026-10-11 08:12:14 -07:00
Malte Ubl bfbff5397c Install Polly in the custom sandbox image
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.
2026-10-11 08:09:10 -07:00
Chris Tate 003265a1df Merge pull request #852 from vercel-labs/fix/publish-leaks
Tell LeakSanitizer that published objects are immortal
2026-10-11 09:10:50 -05:00
Chris Tate 8d5b8a59af Merge pull request #851 from vercel-labs/perf/map-clear
runtime: make Map/Set clear and delete cost O(live), not O(peak)
2026-10-11 09:10:17 -05:00
Malte Ubl 4f89dfd19d tests: workers that drop tens of megabytes and later workers reusing their arenas
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.
2026-10-11 07:08:22 -07:00
Malte Ubl 211b3f22b3 runtime: return an exiting worker thread's free pages to the system
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.
2026-10-11 07:08:22 -07:00
Malte Ubl 9b98571b4a Retain string-keyed map reads inline
Map.get of a reference value under a string key called scr_map_get_str_ref,
which retained the hit through the collection's value adapter: an indirect
tail call into an out-of-line retain function on every hit (in tsc-ts the
symbol tables' Symbol and Type retains, 0.4% of samples, were only reached
that way). Number keys already read the entry and retain with the value
type's own retain. String keys now do the same: scr_map_entry_str returns the
entry, and the generated code loads the value slot and retains it inline.
Both the ordinary and the stack-boxed (local) reads take the entry path.

Corpus 4651 keeps string-keyed results alive past delete, overwrite, clear
and the map itself for instances, records, arrays, nested maps, cycles and
union values; dropping the inline retain makes it (and 523) fail under ASan.
2026-10-11 06:42:21 -07:00
Malte Ubl ea7ec561da Grow the descriptor table before worker threads share it
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.
2026-10-11 06:42:16 -07:00
Malte Ubl 63c45c99ee Tell LeakSanitizer that published objects are immortal
Publication (@scriptc/threads publish() and the zero-copy delivery of
deeply frozen plain-data graphs) makes objects immortal: nothing frees
them, and the RC audit already stops counting them. LeakSanitizer did not
know, so a program that drops its last reference to a published graph
reported every such object as a direct leak whenever leak detection was
on (SCRIPTC_SAN=1 runs on Linux without ASAN_OPTIONS=detect_leaks=0, such
as the documented local fallback). Both publication paths now mark each
object they make immortal with the weak __lsan_ignore_object hook in ASan
builds; ignored chunks are leak-scan roots, so the verdict on the rest of
the heap is unchanged. Other builds compile the hook away.

The sanitized lanes run with detect_leaks=0, so a harness test pins the
contract with LeakSanitizer on (Linux, SCRIPTC_SAN=1 lane).
2026-10-11 06:39:41 -07:00
Malte Ubl 10e10061a6 runtime: keep a map's storage when clear() finds no entries
An empty map's tables are already empty, so clear() returns at once
instead of freeing oversized storage that the next fill would regrow.
Only a clear that removes entries frees storage far larger than them.
The compaction test in delete is marked unlikely.
2026-10-11 06:39:40 -07:00
Malte Ubl 714afdcfe5 runtime: size a cloned map's direct index for its own keys
new Map(m) and new Set(s) copied the source's direct-index size, which
follows the source's peak. The copy now covers its largest live key, and
keys that are sparse for their count hash instead.
2026-10-11 06:39:40 -07:00
Malte Ubl 175d79ff01 test: record the TS7 preflight/order baseline for 4598-map-set-clear-reuse 2026-10-11 06:39:40 -07:00
Malte Ubl 7956559ed1 runtime: make Map/Set clear and delete cost O(live), not O(peak)
Number-keyed maps index small integer keys through a direct table sized
by the largest key ever stored, and bucket tables are sized by the most
entries ever held; neither shrank. scr_map_clear memset both on every
call, even on an empty map, so a collection that was large once paid its
peak size on every later clear. tsc-ts' Relater.reset clears a
Set<number> on every relation check: on Sentry, 36% of the CPU was that
memset.

- clear() resets only the direct-index slots of the entries it removes
  (the table is zero at every absent key), resets the bucket table only
  when entries pointed into it and it is proportional to them, and frees
  storage far larger than the entries removed, returning the map to its
  initial state as V8 does. Clearing an empty map does no work.
- delete compacts once fewer than a quarter of at least 64 entries are
  live (V8's shrink point), so iteration after mass deletion is O(live).
- Compaction shrinks oversized bucket tables, and after a delete or an
  iteration it shrinks the direct index to the surviving keys or moves
  keys that became sparse back to hashing. The append path keeps the
  current index: it holds a slot of it.

Insertion order and live iteration are unchanged: nothing compacts while
an iteration is active.
2026-10-11 06:39:39 -07:00
Malte Ubl 6b0b502303 Return the shared live adapter from DynHost
DynHost.liveDynRefAdapter was declared to return { snapshot; commit }
while LlEmitter returns its cached LlStreamTypedRefAdapter. Under Node
LlDyn received that same object and bindLiveArrayOps found arrayOps; the
native compiler width-copied the record into the narrower shape, dropped
arrayOps and emitted no scr_dyn_typed_ref_bind_array call, so native and
Node builds of the compiler emitted different LLVM. Declare the real
return type so both pass the shared record.
2026-10-11 06:39:38 -07:00
Malte Ubl 79d6abd42e Pass the shape host to bindLiveArrayOps
bindLiveArrayOps took an anonymous `{ declare(line: string): void }` and
four callers passed the LlEmitter class instance. The self-hosted
compiler projects a class instance into a record only from plain fields,
not methods, so the self-hosting IR check rejected each call with SC2002.
Type the parameter as ShapeHost and pass `host.shapeHost`, as the
neighbouring vAdapters/typedRefConstructor calls already do. The emitted
LLVM is unchanged.
2026-10-11 06:39:38 -07:00
Chris Tate 918389d62a Merge pull request #847 from vercel-labs/fix/stream-ir-serialization
Stream serialized IR so compiler-scale artifacts no longer hit V8's string limit
2026-10-10 23:21:07 -05:00
Malte Ubl a9d294d1c1 Widen the old level's growth fraction two doublings at a time
Full passes are scheduled by heap growth, and the fraction they wait for
widens while growth-triggered passes keep finding nothing. Widening went one
doubling per pass after two passes in a row had asked for it, so a heap that
grows for a whole run never reached the cap of 16 that #833 introduced: on
tsc-ts, runs with seven full passes widened to 9x at the sixth pass, and the
seventh still walked 2.7-3.1M objects (294-393 ms) late in the run.

The old level now widens two doublings per qualifying pass, never past what
the pass's yield asks for, so passes that keep finding nothing reach the cap
on the fourth pass instead of the sixth, while the heap is still small.
Narrowing is unchanged: a pass that finds old garbage moves the fraction
straight to what its yield asks for, and widening still waits for two passes
in a row. The mature level keeps a doubling per pass: a mature pass walks only
what was promoted since the last one, so its total walking hardly depends on
how passes are spaced, and narrower spacing lets less young garbage float.

The memory bound is #833's: once passes stop finding garbage, the heap may
grow to 17 times its size after the last full pass before the next. It is
now reached after four such passes instead of six. Jumping straight to the
cap after two was also measured; it reaches the bound at heaps of a few
thousand objects and is not better on tsc-ts at --checkers 8.

test_cycle pins the schedule from where an old-garbage ring narrows the
fraction: the pass after it does not widen, the second pass in a row that
finds nothing reaches the cap (one doubling per pass fails here at caps 1
and 16), and a capped pass comes exactly at the cap. cycle.test.ts also runs
SCR_CYCLE_GROWTH_CAP=8.

Measured with tsc-ts origin/parallel-check e469e33 (--optimization=speed)
against the parent commit, interleaved in Linux Vercel Sandboxes (8 vCPU),
medians; diagnostics and exit codes identical. The build is the next
commit's with its parallel walk off (SCR_CYCLE_PARALLEL=0), which runs this
commit's collector. RSS in MB:

  single-threaded (10 rounds)   wall              RSS
  Excalidraw                    7.94 -> 7.32  -7.8%   1263 -> 1280
  Playwright                    7.09 -> 6.90  -2.7%   1457 -> 1470
  TypeORM                       6.67 -> 6.39  -4.1%   1014 -> 1046
  tsc-ts self-check             4.33 -> 4.03  -6.8%    626 ->  622

  --checkers 8 (10 rounds, second host)
  Excalidraw                    2.56 -> 2.55  -0.6%   2120 -> 2200
  Playwright                    2.46 -> 2.39  -3.0%   2649 -> 2686
  TypeORM                       1.96 -> 1.85  -5.4%   1270 -> 1315
  tsc-ts self-check             1.04 -> 1.04  +0.5%    742 ->  744

Pass log (instrumented build, one single-threaded run each): full-pass time
337 -> 89 ms (self-check), 426 -> 130 ms (TypeORM), 323 -> 104 ms
(Excalidraw); the largest full pass now walks 1.1-1.3M objects early in the
run, and total collector time drops by 27-35%.
2026-10-10 20:45:39 -07:00
Malte Ubl 5b8661bf8c Keep native compiler IR and frontend cache entries out of single strings
Scriptc-compiled strings stop at 2^31 - 1 UTF-8 bytes and exceeding the
cap traps ("scriptc: out of memory") instead of throwing. In the native
compiler the self-hosted IR was one 515 MB string, and the frontend
cache entry JSON.stringify'd LLVM and IR together into one ~920 MB
string, about 2.3x below the trap.

- NativeExecutableInput.ir is compact IR as ~16M-char parts; every IR
  output is written with writeTextPartsSync.
- Frontend cache entries move to scriptc.native-frontend.v3: the input
  metadata carries placeholders and the LLVM and IR follow as separate
  members, so the entry is written as parts with an incremental digest
  (NativeCache.writeParts). Entries a native reader could not decode as
  one string are neither published nor read (NativeCache.read maxBytes),
  turning the old trap into a cache miss.
- SCRIPTC_BOOTSTRAP_IR_INPUT decodes the seed IR per function with
  deserializeModuleBytes.

The native LLVM text remains one string (344 MB for the compiler,
~6x below the native cap).
2026-10-10 20:39:35 -07:00
Malte Ubl 20b7da29e7 Stream serialized IR and LLVM instead of building one string
The self-hosted compiler's compact IR is ~515-528M characters, a few
million below V8's maximum string length (2^29 - 24). serializeModule
joined it into one string, deserializeModule parsed one string, and the
native bootstrap comparison read cli.ir.json and cli.ll as utf8 strings,
so any compiler growth crashed --emit=ir and the bootstrap tests with
"RangeError: Invalid string length".

- serializeModuleParts returns the compact encoding as ordered parts
  (header, one part per function, remainder). serializeModule(mod, true)
  is their join, so the on-disk format is byte-identical.
- deserializeModuleBytes splits a UTF-8 Buffer at the root object's
  "functions" array with a byte-level scanner and parses the header and
  each function separately. Malformed input falls back to the
  whole-document parse for its exact error.
- The Node API writes --emit=ir, --emit-ir sidecars and --emit=llvm
  through writeTextParts (the former writeLlvmSource batching writer).
- The native bootstrap harness decodes IR per function and compares
  LLVM as Buffers, removing ABI-check lines at the byte level.
- The self-hosting serialization fixture gains a "bytes" mode so the
  scriptc-compiled serializer exercises both new entry points.

Self-hosted compiler IR (514.6 MB, macOS arm64, Node 24.12):
  read  old 5.85 s / 4.26 GB max RSS -> new 5.84 s / 1.36 GB
  write old 2.05-2.53 s / 4.48 GB    -> new 2.30-2.37 s / 2.10 GB
  2x module (1.03 GB): old serialize and read throw; new round-trips.
The remaining per-string bound is the largest function (16.3M chars,
~33x headroom) and the header (2.3M chars).
2026-10-10 20:39:35 -07:00
Chris Tate 059b0212e3 Merge pull request #845 from vercel-labs/perf/parser-hot-paths
Run counted loops from an unproven start as integer loops
2026-10-10 21:28:34 -05:00
Chris Tate 4ff3a743ec Merge pull request #843 from vercel-labs/perf/final-field-borrows
Borrow final reference fields without a retain
2026-10-10 21:28:01 -05:00
Chris Tate 9b460892a5 Merge pull request #841 from vercel-labs/perf/checker-hot-paths
Count module TDZ checks, atomic counters and array appends as edge-preserving
2026-10-10 21:27:35 -05:00
Chris Tate 3afea88bfb Merge pull request #839 from vercel-labs/perf/length-to-double-nneg
Convert lengths and in-range indices to doubles as non-negative
2026-10-10 21:27:14 -05:00
Chris Tate e44f50b327 Merge pull request #837 from vercel-labs/perf/trace-cycle-candidates
Trace only types that can lie on a cycle
2026-10-10 21:26:29 -05:00
Chris Tate 0b0b7a8319 Merge pull request #835 from vercel-labs/fix/number-undefined-unions
Keep missing reads undefined through number arguments, narrowed slots and lambda results
2026-10-10 21:25:54 -05:00