Commit Graph
1095 Commits
Author SHA1 Message Date
Chris Tate 269c4130f5 Fix console ordering checks under pipe backpressure
- Keep merged-pipe ordering probes small enough for deterministic Node output.
- Preserve large-write coverage for shared files and independent streams.
2026-10-08 18:21:31 -05:00
Chris Tate 8bd17409db Sync runtime and compiler updates into canary
- Merge main's runtime optimizations and CI changes with canary's frontend improvements.
- Preserve adaptive cycle reclamation with inline allocation and deallocation.
- Repair native bootstrap callback narrowing and emitter expectations.
2026-10-08 17:41:20 -05:00
Chris Tate 13633c08b6 Merge pull request #772 from vercel-labs/fix/method-census-data-writes
Fix compiling tsc-ts: keep data writes out of the method replacement census
2026-10-08 16:48:36 -05:00
Malte Ubl 723e9f0880 Merge pull request #764 from vercel-labs/perf/rebased/22-h46-stdout-fast
Write console and stream output directly to the descriptor (log-lines -11%)
2026-10-08 16:41:42 -05:00
Malte Ubl 2625ae9c9e Keep data writes out of the class method replacement census
The method-replacement census marks a method name as an observed slot
when some write might replace it, and calls of observed methods go
through a checked property lookup whose result converts back to the
declared return type. Two kinds of data writes over-approximated that:

- A computed write into a string-keyed dictionary marked every method in
  the program unless the dictionary's values were primitives. Class
  instances have no implicit index signature, so such a dictionary is a
  class view only when the class declares the index signature, and then
  every method must be assignable to its value type. Any value type no
  function can inhabit now qualifies: primitives, or non-callable object
  types that require a property functions lack, or weak types sharing no
  property with functions.
- A named write like `this.literal = token` marked `literal` for every
  class. The write can only replace a method when the written property
  can hold a function, so property writes are now judged by the written
  property's type (prefetched in one batch).

Compiling vercel-labs/tsc-ts failed after the canary promotion: one
`changedProjects[id] = {...}` write marked every method, OptionNameMap.get
and the JSON parser's literal() calls returned unknown, and their Map-typed
results could not convert back (SC1100/SC1101, 61 errors).

Also skip abstract methods when materializing observed prototype slots.
An abstract declaration emits no prototype member, so lookup continues
to the ancestors; building its value instead threw an uncaught
PoisonError out of the reachability fixed point (the tsc-ts crash).

Tests: corpus 3171 (fails with SC1100/SC1101 before this change) matches
Node in the plain and sanitized lanes; the 112 corpus programs matching
abstract|prototype|method|callback pass; class-dynamic-dispatch unit
tests pass. tsc-ts now builds and type-checks itself with output
identical to tsgo 7.0.2.
2026-10-08 14:32:33 -07:00
Malte Ubl 3fe49589ee Record preflight/order baselines for corpus 4294-4296
The TypeScript 7 parity job needs baselines for the console corpus
programs this PR adds (4294-console-line-sizes, 4295-console-template-
parts, and 4296-console-glued-long-parts from the overflow fix).
Recorded with SCRIPTC_UPDATE_BASELINES=1 pnpm exec vitest run
packages/compiler/test/ts7/order-parity.test.ts. The re-recorded file
added only these three and #763's 3170, with no existing entry changed.
The 3170 entry is left to #763. These are inserted at their sorted
position, apart from 3170, so the hunks stay independent when the stack
is rebased.

node scripts/test-ts7.mjs --baselines-only now reports only 3170, which
the rebased base branch provides.
2026-10-08 15:51:42 -05:00
Malte Ubl 62ff176ad7 Mask console argument tags when sizing the line buffer
scr_console_capacity compared the raw tag with SCR_ARG_STR, but string
parts of a concatenation argument carry SCR_ARG_GLUE. A glued string part
therefore reserved only 32 bytes while scr_console_render, which masks
with SCR_ARG_KIND, copied its full length, overflowing the on-stack (or
heap) line buffer for any glued part longer than 32 bytes.

Mask with SCR_ARG_KIND in the capacity loop and in the single-string
fast path, the two remaining kind tests in scr_console.c.

Tests: corpus 4296 logs template literals and `+` chains with glued
string parts of 31..70000 bytes, several long parts per argument, and
multi-byte UTF-8 parts. Before this change the --sanitize build aborted
with an AddressSanitizer stack-buffer-overflow; it now matches Node in
the plain and sanitized lanes.
2026-10-08 15:51:42 -05:00
Malte Ubl 0cd28f7d45 Pass string-concatenation console arguments as parts
console.log(`${method} ${path} ${status}`) used to build the joined string
first (one string per number, scr_str_concat_parts, then releases), and
the console then copied that string into the line it writes. The LLVM
backend now flattens a strConcat argument with stringParts and passes
each part as a ScrLogArg:

- Later parts carry SCR_ARG_GLUE, so no separating space is added.
- A number conversion passes the raw double as SCR_ARG_NUM (String()
  spelling, so -0 prints "0"; a bare number argument keeps inspect's
  "-0").
- A boolean conversion passes the bool.

The runtime renders the parts directly into the line buffer. Parts are
evaluated in source order through emitStringInputs, which applies the
same borrowing rules as concatenation. Calls that use the new tags go to
new scr_console_log_parts / scr_console_error_parts entry points, so a
program object linked against an older runtime pack fails at link time
instead of misprinting.

Tests: corpus 4295 covers -0/NaN/exponent spellings inside templates
next to bare arguments, empty parts, more than 16 parts, evaluation
order, a later part reassigning an earlier string, union and optional
values, and async code. A new co-located IR unit test checks the
emitted tags and entry points.

Linux sandbox, ns per line (median of 9 launches, 200k lines), prev =
stack tip, cand = this commit (with the previous one). All outputs
byte-identical to Node.

  probe      target  node   prev  cand
  p-str      pipe    4022   1364  1149
  p-str      file    3376    849   744
  p-str      null    2360    330   247
  p-args     pipe    6687   1441  1343
  p-args     file    5135    969   821
  p-args     null    3831    389   292
  p-write    pipe    2866   1244  1168
  p-write    file    2329    839   782
  p-write    null    1622    309   275
  p-err      pipe    4656   3253  1073
  p-err      file    3659   2099   721
  p-err      null    2749    591   219
  log-lines  pipe    5007   1476  1314
  log-lines  file    3949    965   877
  log-lines  null    3145    446   378
2026-10-08 15:51:42 -05:00
Malte Ubl c48f059071 Write console and process stream output directly to the descriptor
Every console.log/error/warn line and process.stdout/stderr.write chunk
is still submitted before the call returns, as in Node. On POSIX
executables, each one now goes out as exactly one write(2) on fd 1 or 2.
Before, it went through C stdio, which cost a copy into the stdio
buffer, one stream call per argument, separator, and newline, and a
flush. stderr is unbuffered, so console.error made one write(2) per
piece: 3 writes per line on average in the probe.

- The line renders into a 2 KiB stack buffer (heap above that). A large
  single string goes out with writev and no copy.
- Short writes continue and EINTR retries. Other errors (EAGAIN, EPIPE,
  EBADF) return errno with the rest dropped, which is what the stdio
  flush did. console still ignores them, and SIGPIPE behavior is
  unchanged.
- Runtime-internal bytes still in the C stdout buffer are flushed first
  (__fpending on Linux, the FILE fields on Darwin, an unconditional
  flush elsewhere), so stdout order and merged 2>&1 order are kept.
- Windows, WASI, and library artifacts keep the C stream path, now fed
  one formatted chunk per call.

Tests: corpus 4294 covers line sizes across the stack buffer and the
64 KiB stdio buffer. New console-io harness cases cover stdout and
stderr merged into one pipe and into one file (byte-compared with
Node), and console.log after the stdout reader closes (EPIPE).

Linux sandbox, ns per line (median of 9 launches, 200k lines; scriptc
includes ~1 ms startup). All outputs byte-identical to Node.

  probe      target  node   prev  cand
  p-str      pipe    4040   1323  1239
  p-str      file    3092    849   790
  p-str      null    2193    319   283
  p-args     pipe    6168   1440  1228
  p-args     file    4896    925   802
  p-args     null    3916    389   292
  p-write    pipe    2889   1328  1197
  p-write    file    2267    809   769
  p-write    null    1577    297   265
  p-err      pipe    4672   3253  1132
  p-err      file    3478   2042   717
  p-err      null    2614    562   232
  log-lines  pipe    4958   1490  1356
  log-lines  file    3885    966   877
  log-lines  null    2983    425   368

strace -c (pipe): p-err went from 600,000 to 200,000 write calls. The
other probes stay at one write per line.
2026-10-08 15:51:42 -05:00
Malte Ubl 1d552913ec Add a logging-heavy runtime benchmark workload
log-lines writes 200k lines (scale 4) through the common console shapes:
a single template literal, string plus number arguments, several
primitive arguments, process.stdout.write chunks, and an occasional
console.error line. The runner captures stdout through a pipe, so the
workload measures immediate per-line writes into a pipe.
2026-10-08 15:51:42 -05:00
Malte Ubl cbdd02ac84 Merge pull request #763 from vercel-labs/perf/rebased/21-h07-numbers
Add fast paths for number and string conversion (csv-numbers -30%)
2026-10-08 15:51:37 -05:00
Malte Ubl 4c1cc612be Record the preflight/order baseline for corpus 3170
The TypeScript 7 parity job reported tests/corpus/3170-number-conversion-
fast-paths.ts as missing a baseline. Recorded with
SCRIPTC_UPDATE_BASELINES=1 pnpm exec vitest run
packages/compiler/test/ts7/order-parity.test.ts: the re-recorded file
added only this program (and the later stack's console programs, which
belong to #764), with no existing entry changed. The entry is inserted
at its sorted position in the stack's baseline block so it does not
overlap the #764 entries when the stack is rebased.

node scripts/test-ts7.mjs --baselines-only passes.
2026-10-08 15:49:53 -05:00
Malte Ubl e7eef59ec1 Fast paths for number<->string conversion (Number/parseFloat, toFixed, two-decimal String)
String to number: Number(str), unary +, and parseFloat now convert short
decimal spellings with Clinger's exact fast path (scr_decimal_scan in
scr_number.c): significant digits m <= 2^53 and a decimal exponent within
+-22 (plus the folded range up to 22 + 15) need one correctly rounded
multiply or divide, bit-identical to strtod. ToNumber validates and
accumulates in the same pass; everything else still goes through the copied
span and strtod. Whitespace trimming in Number(), parseInt, and parseFloat
skips the UTF-8 decode for ASCII non-whitespace.

toFixed: the exact-binary-value digit generation moves from scr_lib.c into
scr_number.c (scr_f64_to_fixed). For f <= 22, mantissa * 5^f fits a
64x64->128 multiply, so the spec's n is one shift plus the dropped-half bit
whenever it fits 64 bits. Digits render two at a time instead of through
snprintf. The 16-limb bignum remains the fallback.

Number to string (H44): a non-integral x below 2^46 that is the double
nearest to m/100 prints as m/100 directly instead of running Ryu. Below 2^46
ulp(x) < 0.01, so m/100 is the unique shortest round-tripping decimal. The
correctly rounded division (double)m / 100 == x is the proof. This covers
JSON.stringify, templates, String(), join, and console output.

Tests: Node oracles for every path. number-cases.txt gains money-like values,
the 2^46 cutoff, and one-ulp neighbors. tonumber-cases.txt gains fast-path
boundaries plus 20k seeded short decimals. The new parsefloat-cases.txt pins
scr_parse_float, and the new fixed-cases.txt/test_fixed.c pin toFixed,
including the 2^64 fast-path limit and f up to 100. Fuzz modes cover all four
paths. Existing case lines are unchanged. Corpus 3170 pins the paths
end-to-end against Node.

Linux x64 sandbox, A/B against perf/stack/06-h23-layout-noise,
--layouts=4 --runs=16 (run 3 of 3; geomean -6.0% / -6.7% / -6.3%):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 236.2 | 204.2 | -13.5% | -19.8% .. +2.1% | neutral | +0.0% | 488.62 | 0.42x |
| regex-logs | 176.2 | 168.8 | -4.2% | -10.1% .. +0.3% | neutral | +0.0% | 281.23 | 0.60x |
| ast-interp | 280.0 | 273.1 | -2.5% | -5.6% .. +1.4% | neutral | +2.8% | 273.05 | 1.00x |
| records-sort | 952.4 | 939.4 | -1.4% | -5.0% .. +5.6% | neutral | +0.0% | 289.87 | 3.24x |
| template-render | 78.9 | 77.3 | -2.1% | -6.2% .. +1.2% | neutral | +0.0% | 246.71 | 0.31x |
| functional-pipeline | 338.7 | 348.8 | +3.0% | -6.5% .. +24.0% | neutral | +0.1% | 339.84 | 1.03x |
| async-pipeline | 207.3 | 207.8 | +0.2% | -5.3% .. +3.8% | neutral | +0.0% | 105.57 | 1.97x |
| word-graph | 302.0 | 300.1 | -0.6% | -5.1% .. +12.9% | neutral | +3.0% | 600.48 | 0.50x |
| alloc-trees | 336.3 | 332.4 | -1.1% | -6.2% .. +2.6% | neutral | +0.1% | 144.73 | 2.30x |
| numeric-kernels | 49.7 | 49.8 | +0.1% | -3.4% .. +6.5% | neutral | +4.0% | 143.73 | 0.35x |
| validate-errors | 179.1 | 182.9 | +2.1% | -5.8% .. +9.1% | neutral | +0.0% | 727.78 | 0.25x |
| csv-numbers | 349.0 | 233.8 | -33.0% | -37.5% .. -26.5% | faster | +0.1% | 541.77 | 0.43x |
| log-summary | 171.6 | 135.7 | -20.9% | -23.3% .. -17.7% | faster | +1.8% | 313.2 | 0.43x |
| inventory-report | 411.9 | 384.7 | -6.6% | -18.1% .. +5.5% | neutral | +2.7% | 521.27 | 0.74x |

Size: +64..+136 bytes per executable. The +1.8..+4.0% rows are one 4 KiB
page crossing (two for inventory-report).
2026-10-08 15:49:53 -05:00
Malte Ubl f9f7880860 Merge pull request #762 from vercel-labs/perf/rebased/20-h40-scaling-probes
Add a scaling probe suite for asymptotic and syscall pathologies
2026-10-08 15:49:49 -05:00
Malte Ubl 1c5dfda937 Add a scaling probe suite and strace tools for pathology hunting
benchmarks/scaling: 128 probes of common operations (arrays, strings,
Map/Set/records, JSON, closures, exceptions, regex, dates, promises,
timers, console, fs) run at 3-4 sizes under scriptc and Node with
identical-stdout checks; run.mjs flags superlinear growth and >3x Node,
--strace-only reports syscalls per operation, strace-workloads.mjs
straces the runtime benchmark suite. Results and the full probe table:
results/h40-pathology-hunt/REPORT.md.

Pathologies fixed on this branch (probe ms at 1e3/1e4/1e5/1e6, Linux):
  i in number[]        base 43/5291/timeout   -> 0.03/0.20/1.5/15   (Node 0.19/0.73/6.8/39)
  k in Record<string>  base 4.8/436/timeout   -> 0.15/1.5/20/319    (Node 0.96/8.9/100/1452)
  re.exec /g loop      base 1.1/70/7910/t.o.  -> 0.49/4.1/40/416    (Node 0.47/4.6/43/398)
  clearTimeout n live  base 1.3/10/1014/t.o.  -> 1.3/3.9/38/740     (Node 3.8/9.4/46/527)
  new RegExp repeat    base 3.9/38/368/3811   -> 0.13/1.1/11/106    (Node 0.49/2.9/23/174)
  console.error        2 writes/line          -> 1 write/line

Suite A/B, Linux x86_64 sandbox, candidate vs perf/stack/06 (--layouts=4
--runs=16), all workloads ok:

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 202.3 | 201.9 | -0.2% | -7.0% .. +3.8% | neutral | +0.0% | 458.29 | 0.44x |
| regex-logs | 166.8 | 166.4 | -0.2% | -1.6% .. +1.1% | neutral | +0.1% | 239.97 | 0.69x |
| ast-interp | 268.6 | 269.9 | +0.5% | -4.6% .. +3.8% | neutral | +0.0% | 242.7 | 1.11x |
| records-sort | 648.7 | 670.4 | +3.3% | -4.7% .. +10.0% | neutral | +0.0% | 222.69 | 3.01x |
| template-render | 69.6 | 70.7 | +1.7% | -0.2% .. +2.6% | neutral | +0.0% | 187.3 | 0.38x |
| functional-pipeline | 221.8 | 214.9 | -3.1% | -7.3% .. +0.7% | neutral | +0.0% | 249.06 | 0.86x |
| async-pipeline | 197.3 | 198.2 | +0.4% | -1.2% .. +3.0% | neutral | +0.2% | 89.25 | 2.22x |
| word-graph | 259.2 | 256.0 | -1.3% | -6.3% .. +3.0% | neutral | +0.0% | 469.48 | 0.55x |
| alloc-trees | 318.8 | 316.8 | -0.6% | -3.0% .. +1.2% | neutral | +0.0% | 127.68 | 2.48x |
| numeric-kernels | 46.4 | 46.5 | +0.1% | -1.6% .. +3.9% | neutral | +0.0% | 133.42 | 0.35x |
| validate-errors | 176.5 | 178.1 | +0.9% | -3.1% .. +4.7% | neutral | +0.0% | 702.45 | 0.25x |
| csv-numbers | 336.2 | 336.8 | +0.2% | -2.0% .. +1.2% | neutral | +0.0% | 500.14 | 0.67x |
| log-summary | 165.7 | 164.6 | -0.6% | -5.9% .. +1.9% | neutral | +0.0% | 290.81 | 0.57x |
| inventory-report | 321.9 | 320.1 | -0.6% | -6.7% .. +9.5% | neutral | +0.0% | 431.4 | 0.74x |

geomean change: +0.0%
geomean candidate/node: 0.768x (lower is better; goal: well below 1)

macOS lane not run (all measurement in the sandbox per task instructions).
2026-10-08 15:48:28 -05:00
Malte Ubl 40bc638aeb Merge pull request #761 from vercel-labs/perf/rebased/19-h45-speed-posture
Add an opt-in --optimization=speed posture (a further -9% on Linux)
2026-10-08 15:48:24 -05:00
Malte Ubl 66e82da225 Record preflight/order baselines for corpus 4032 2026-10-08 15:46:34 -05:00
Malte Ubl 90d024cd8c Keep inline RC helper enumeration inside the self-hosted subset
The bootstrap-seed build rejected `host.rcHelpers ?? []` in shapes.ts
(SC2011: `Set<string> | never[]` has no static representation) and the
spread of that union into a string[] literal (SC1090). A small helper copies
the set's keys into a plain array (empty when inline RC is off), which both
callers iterate; emitInlineRcHelpers still sorts its copy. Behavior is
unchanged.
2026-10-08 15:46:34 -05:00
Malte Ubl 2076f793f6 Keep 64-byte x86 runtime alignment for --optimization=speed only
The default build's largest size contributor on the stack was H23's
-falign-functions=64 on every x86-64 runtime unit (2-11 KB per program;
results/h45-size-budget). Release and library flavors now keep the
compiler's default function alignment; the opt-in speed flavor keeps the
blanket 64-byte alignment. Vendored archives stay 64-byte aligned
(lre_exec keeps its pinned placement) except libunicode.c, whose small
table helpers were most of the regex programs' padding. The runtime-first
link order is unchanged.

Docs and usage text describe speed without the reverted runtime profile.
2026-10-08 15:46:34 -05:00
Malte Ubl cd5e46843c bench-runtime: separate the workload and posture help paragraphs 2026-10-08 15:46:34 -05:00
Malte Ubl 2d6a239abb Skip buffering untraced union boxes in the inline union release
The speed posture's inline scr_union_release fast path predates the
runtime change that buffers a surviving union box as a cycle candidate
only when its arm_trace can reach a cycle. It still enqueued every
surviving box: sound (an untraced box visits nothing and the runtime death
path reads the header), but it gave back that change's collector savings
under --optimization=speed. The fast path now loads arm_trace and returns
when it is NULL, exactly like scr_union_release; scr_runtime.h pins the
offset.
2026-10-08 15:46:34 -05:00
Malte Ubl 99566b2dfb Document --optimization=speed on the CLI page
Final numbers for the speed posture (H30 inline RC + H02 runtime bitcode import + H28 runtime PGO) against release on the same tree. Release executables are byte-identical to perf/stack/06-h23-layout-noise on both lanes (14/14).

Linux x86_64 sandbox, --layouts=4 --runs=16 (linux-speed-vs-release.json):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size (bytes) | cold build ms | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | ---: |
| json-records | 194.4 | 164.7 | -15.3% | -17.3% .. -13.7% | faster | +18.3% (239328 → 283176) | 1311 → 1987 | 439.51 | 0.38x |
| regex-logs | 165.9 | 147.6 | -11.0% | -12.4% .. -7.1% | faster | +11.2% (325648 → 362136) | 1003 → 1539 | 235.96 | 0.63x |
| ast-interp | 271.7 | 200.8 | -26.1% | -27.7% .. -22.9% | faster | +14.9% (149032 → 171264) | 1230 → 1797 | 243.44 | 0.82x |
| records-sort | 669.8 | 553.9 | -17.3% | -22.0% .. -10.8% | faster | +24.2% (140840 → 174960) | 1232 → 1829 | 226.37 | 2.45x |
| template-render | 70.5 | 62.9 | -10.7% | -12.9% .. -7.1% | faster | +4.1% (244976 → 254968) | 1049 → 1550 | 193.97 | 0.32x |
| functional-pipeline | 220.8 | 189.3 | -14.3% | -19.8% .. -10.4% | faster | +23.1% (112928 → 139008) | 1130 → 1841 | 269.15 | 0.70x |
| async-pipeline | 197.0 | 196.8 | -0.1% | -2.4% .. +5.3% | neutral | +7.3% (169944 → 182392) | 978 → 1297 | 91.97 | 2.14x |
| word-graph | 251.9 | 231.1 | -8.3% | -14.4% .. -4.6% | faster | +30.7% (139040 → 181664) | 1279 → 1974 | 471.96 | 0.49x |
| alloc-trees | 321.6 | 271.4 | -15.6% | -18.2% .. -13.3% | faster | +10.8% (88672 → 98280) | 946 → 1281 | 125.46 | 2.16x |
| numeric-kernels | 51.2 | 48.3 | -5.5% | -17.4% .. +4.9% | neutral | +16.8% (106600 → 124488) | 1061 → 1411 | 147.98 | 0.33x |
| validate-errors | 188.9 | 163.0 | -13.7% | -23.3% .. +10.3% | neutral | +8.2% (285424 → 308904) | 991 → 1447 | 793.58 | 0.20x |
| csv-numbers | 357.6 | 317.1 | -11.3% | -15.9% .. -7.6% | faster | +21.1% (146856 → 177792) | 986 → 1572 | 541.06 | 0.59x |
| log-summary | 167.6 | 157.4 | -6.1% | -9.5% .. -1.3% | faster | +30.5% (225256 → 294024) | 1331 → 2143 | 311.61 | 0.51x |
| inventory-report | 327.6 | 263.5 | -19.6% | -28.2% .. -15.3% | faster | +22.2% (311336 → 380312) | 1599 → 2410 | 445.01 | 0.59x |

contenders: baseline=release@, candidate=speed@; layouts 4, runs 16
geomean change -12.7%; faster 11/14; slower: none; size geomean +17.1% (max +30.7%); cold build geomean +48.5%; candidate/node 0.658x (baseline/node 0.754x)

macOS arm64 (shared host), --layouts=4 --runs=16 (local-speed-vs-release.json; file sizes move in 16 KiB pages, __text geomean +30.8%):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size (bytes) | cold build ms | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | ---: |
| json-records | 113.7 | 89.4 | -21.4% | -22.4% .. -20.5% | faster | +28.8% (222592 → 286688) | 958 → 1523 | 224.67 | 0.40x |
| regex-logs | 106.0 | 91.6 | -13.5% | -15.4% .. -11.7% | faster | +22.7% (279128 → 342456) | 996 → 1106 | 129.92 | 0.70x |
| ast-interp | 164.6 | 111.7 | -32.1% | -32.9% .. -31.4% | faster | +17.8% (148376 → 174760) | 896 → 1288 | 136.99 | 0.82x |
| records-sort | 369.7 | 286.4 | -22.5% | -26.6% .. -18.9% | faster | +17.8% (147888 → 174192) | 980 → 1244 | 133.39 | 2.15x |
| template-render | 50.4 | 46.6 | -7.4% | -9.6% .. -5.9% | faster | +13.0% (205808 → 232512) | 942 → 1039 | 95.49 | 0.49x |
| functional-pipeline | 129.9 | 104.5 | -19.6% | -22.3% .. -15.1% | faster | +36.0% (112464 → 152912) | 872 → 1523 | 120.64 | 0.87x |
| async-pipeline | 261.6 | 261.8 | +0.1% | -2.2% .. +3.4% | neutral | +21.4% (154624 → 187776) | 750 → 962 | 57.94 | 4.52x |
| word-graph | 133.1 | 110.7 | -16.9% | -22.0% .. -10.2% | faster | +30.3% (147192 → 191816) | 876 → 1298 | 276.89 | 0.40x |
| alloc-trees | 216.8 | 163.7 | -24.5% | -25.5% .. -23.8% | faster | +25.5% (92792 → 116424) | 746 → 962 | 75.59 | 2.17x |
| numeric-kernels | 32.1 | 31.1 | -2.8% | -5.4% .. -0.5% | faster | +21.8% (110832 → 134960) | 853 → 979 | 78.45 | 0.40x |
| validate-errors | 110.6 | 86.3 | -22.0% | -22.8% .. -21.0% | faster | +31.0% (205136 → 268720) | 784 → 1507 | 428.13 | 0.20x |
| csv-numbers | 184.2 | 160.8 | -12.7% | -17.1% .. -10.6% | faster | +19.9% (146984 → 176200) | 1217 → 1380 | 252.81 | 0.64x |
| log-summary | 88.8 | 72.8 | -17.9% | -19.9% .. -16.3% | faster | +38.7% (205144 → 284520) | 1440 → 1731 | 152.02 | 0.48x |
| inventory-report | 132.7 | 105.4 | -20.6% | -23.3% .. -16.7% | faster | +43.0% (223120 → 319088) | 1472 → 1940 | 199.93 | 0.53x |

contenders: baseline=release@a5a5b961+dirty, candidate=speed@a5a5b961+dirty; layouts 4, runs 16
geomean change -17.1%; faster 13/14; slower: none; size geomean +26.0% (max +43.0%); cold build geomean +34.0%; candidate/node 0.722x (baseline/node 0.871x)

Incremental, Linux: H30 vs release -2.3%; H02 on H30 -6.6%; H28 on H30+H02 -4.4%. Linux text+data speed vs release: geomean +19.0%, max +33.7%. Full report: results/h45-speed-posture/REPORT.md.
2026-10-08 15:46:34 -05:00
Malte Ubl 60c30d6ebd Import runtime bitcode into speed programs (H02)
Re-lands perf/h02-runtime-lto (6d373317, b62448f3) behind the speed
posture.

- Runtime packs gain an executable `speed` flavor whenever the host LLVM
  helper can run. Its static units are emitted through the helper's
  `runtime-unit` command (promoted unit-local symbols, object and import
  bitcode from one module); its SCR_DYNAMIC variants reference the release
  objects. Release, dev, and library flavors and the vendor archives are
  built exactly as before (the rebuilt macOS pack's flavors and archives
  are identical to the baseline's).
- Speed program builds load the speed flavor's bitcode and pass it to the
  helper (`--import-bitcode`); release builds never do.
  loadRuntimeBitcode only reads a real speed flavor, never release.
- The helper drops the emitter's inert sanitize_address attributes only
  when it imports, so release modules reach the pipeline unchanged.
- Tests: runtime-pack.test.ts covers speed-variant bitcode selection,
  verification, and that release-flavor bitcode is never imported;
  native-codegen.test.ts covers helper arguments and cache keys.

Release executables stay byte-identical to the stack tip with the rebuilt
helper (14/14 workloads, macOS).
2026-10-08 15:46:34 -05:00
Malte Ubl 213cf287b9 Emit inline retain/release fast paths under --optimization=speed (H30)
Re-lands perf/h30-inline-rc (00051ca0, 40398773, 07972360) behind the
speed posture. With inline RC on, every mirrored runtime RC family
retains inline and releases inline down to the rc == 1 / new cycle
candidate slow path (shapes.ts INLINE_RC_FAMILIES). Release emission is
unchanged: ShapeHost.rcHelpers is null unless LlvmTargetOptions.inlineRc
is set, and every retain/release (including capture boxes) stays a
runtime call; the %ScrMapRc type is only emitted with inline RC.

- index.ts and native/prepare.ts pass inlineRc for speed executables,
  speed object/asm emission, and speed library profiles.
- scr_runtime.h keeps H30's static asserts for the mirrored header and
  trace-slot offsets (no code change; the rebuilt macOS pack's manifest is
  identical to the baseline's).
- Tests: shapes.test.ts pins release symbols and H30's speed fast paths
  and header offsets; emitter.test.ts pins that release IR is unchanged
  and speed routes releases through the helpers. The differential harness
  gains `// @optimization: speed` (and a SCRIPTC_TEST_OPTIMIZATION=speed
  lane); corpus 4032 pins every mirrored family under speed.

Release executables stay byte-identical to the stack tip (14/14
workloads, macOS).
2026-10-08 15:46:34 -05:00
Malte Ubl 2372b75ec2 Add an opt-in --optimization=speed posture
Speed is a third native optimization posture next to release (default,
-O2) and dev (-O0). It is optimized exactly like release at the C/LLVM
level and is the switch for optimizations that trade executable size and
build time for run-time speed, so the default release build stays inside
the binary-size budget.

- backend/optimization.ts: the NativeOptimization type, its -O class, and
  cache-key helpers. Release keeps its historical key shapes (absent).
- CLI: build/run/cache warm accept speed; usage text names the trade.
  Bootstrap's routed executable cache mirrors the posture.
- Early executable cache keys and native-feature validation include speed,
  so release, dev, and speed entries never satisfy one another.
- Runtime packs: an optional executable `speed` flavor (-O2, only beside
  release). Speed links it when present and the release objects otherwise;
  library modes link their release flavor. The sanitizer lane follows the
  same flavor key.
- Library profiles accept "speed".
- WASI release-only linker flags apply to speed too.
- bench-runtime: --baseline-optimization/--candidate-optimization and a
  per-contender cold_build_ms.

Release executables are byte-identical to the stack tip for all 14
benchmark workloads (macOS arm64, sha256 of executables built from both
trees).
2026-10-08 15:46:34 -05:00
Malte Ubl 631e8d2c33 Merge pull request #760 from vercel-labs/perf/rebased/18-h40-pathology-hunt
Fix quadratic and syscall-heavy runtime paths (up to 7,000x slower than Node to at or below Node)
2026-10-08 15:46:29 -05:00
Malte Ubl bcf9fc21b0 Keep the timer-clearing corpus program fully static
tests/harness/coverage.test.ts requires every corpus program to compile
100% statically. timers-clear-many.ts cleared `handles[i]!`, and the
non-null assertion does not narrow an indexed element for the static
lowering, so the three clearTimeout calls reported SC2020 ("clearTimeout of
'number | undefined' handles"). Read the element into a local and test it
against undefined instead. Node output is unchanged; scriptc coverage now
reports 46/46 statements static and the native binary matches Node.
2026-10-08 15:44:26 -05:00
Malte Ubl 9cf183e053 Record preflight/order baselines for corpus in-array-runtime-index, regex-exec-long-subject, regexp-constructor-repeat, timers-clear-many 2026-10-08 15:44:26 -05:00
Malte Ubl 14bd48fd94 Keep the timer slot index and regex constructor cache per worker
Upstream native workers (095b6193) run one runtime instance per thread and
make the timer heap thread-local (SCR_TL). The timer slot index added for
O(1) handle lookups indexes that heap, so it becomes thread-local too;
scr_timers_teardown already frees it during worker context shutdown.

The RegExp constructor cache stores retained templates that live instances
borrow and that are never evicted. Its table is thread-local, and worker
executables leave it empty (constructions compile privately as before),
because a worker context has no teardown point after every borrowing
instance. Ordinary executables are unchanged.
2026-10-08 15:44:26 -05:00
Malte Ubl 1152644a9c Write each console.error line with one write
stderr is unbuffered, so the piecewise stdio calls issued one write(2)
per argument, separator and newline (two syscalls per simple line), and
the line was not atomic against other writers sharing the descriptor.
Render the line first and emit it with a single fwrite, as Node writes
one chunk.
2026-10-08 15:44:26 -05:00
Malte Ubl 260aad64a7 Index timer handles by heap slot
clearTimeout/clearInterval, refresh, ref/unref and hasRef scanned the
whole timer heap for the handle, so arming and clearing many timers was
quadratic (1e5 cancels: 1.0 s vs Node 46 ms). An id-to-slot hash kept
exact by every heap write makes the lookup O(1); heap order is unchanged.
2026-10-08 15:44:26 -05:00
Malte Ubl 536deb5904 Cache regex subject decoding for exec/test and compiled RegExp patterns
exec/test rescanned the whole subject for ASCII (and re-converted a
non-ASCII subject to UTF-16) on every call, so lastIndex-driven exec
loops and sticky tokenizers were quadratic (1e5 matches: 7.9 s vs Node
43 ms). A single-entry cache keeps the last long subject's decoding; it
retains the subject so the address cannot be recycled and the string is
never appended in place. Only exec/test use it (no user code runs while
their input is live).

new RegExp recompiled its pattern on every construction (22x Node). A
bounded never-evicting cache keyed by (flags, source) shares bytecode
through the literal-template mechanism; invalid patterns still throw on
every construction.
2026-10-08 15:44:26 -05:00
Malte Ubl 93b4994220 Lower runtime-key in over arrays and dictionaries to presence probes
`i in arr` with a non-literal number key boxed the typed array into the
checked-dynamic tree and materialized every element per evaluation, so
`for (...) if (i in arr)` loops were quadratic (1e4 elements: 5.3 s vs
Node 0.7 ms). A number key over an array receiver now lowers to arrayHas
(scr_arr_has), the query the literal fold and Object.hasOwn already use;
the key is stabilized first to keep JS evaluation order.

`k in dict` with a runtime string key over an index-signature record
snapshotted and scanned every overflow key per probe (1e4 keys: 436 ms,
1e5: timeout). The helper now ends in recordOvfHas (scr_map_has_str).
2026-10-08 15:44:26 -05:00
Malte Ubl 665836bad6 Merge pull request #759 from vercel-labs/perf/rebased/17-h35-server-path
Send HTTP response heads with their bodies (http-api -19%, 70k vs Node's 26k req/s)
2026-10-08 15:44:23 -05:00
Malte Ubl 4ea8d52f63 Pin large HTTP bodies through the coalesced head+body write
Multi-megabyte end(data) and chunked write() bodies under a slow raw
reader force partial vectored sends; the driver checks framing, length,
and digest of every response against Node.
2026-10-08 15:42:37 -05:00
Malte Ubl b84f31e65a Send HTTP response heads with their bodies and trim per-request server work
- scr_net: scr_net_sock_writev_native sends ordered slices with one
  sendmsg when the bytes would go out immediately (plain socket, nothing
  buffered); every other state takes the per-slice write_raw path, so
  buffering, transports, and teardown are unchanged.
- scr_http: end(data) before the head, write() before the head, chunk
  framing, and the final chunk now leave as one write (head + body +
  framing), as Node does. A typical response was a head segment plus a
  body segment; it is now one segment and one syscall.
- scr_net: stop reading after a short read (libuv UV_HANDLE_READ_PARTIAL);
  the level-triggered poller reports later bytes or EOF. Removes the
  EAGAIN read per request (0.7 per request on http-api). TLS transports
  keep draining.
- scr_http: Date header formatted once per second; head built in one
  pass without snprintf; request headers parsed with geometric array
  growth and one shared string when the name is already lowercase.

The bytes on the wire are unchanged (the runner digests every response
against Node).
2026-10-08 15:42:37 -05:00
Malte Ubl 2ef2fe45b0 Merge pull request #758 from vercel-labs/perf/rebased/16-h12-string-keys
Cache string Map key hashes in the string header (ast-interp -4%)
2026-10-08 15:42:33 -05:00
Chris Tate dc98e40202 Refresh the surface manifest for import-cycle support (#769) 2026-10-08 15:41:53 -05:00
Malte Ubl 4daa362e55 Record preflight/order baselines for corpus 3141 2026-10-08 15:40:53 -05:00
Malte Ubl 30ba03592e Keep the string key hash mixer inside the self-hosted subset
The bootstrap-seed build rejected the compound `value ^= value >> 30n` and
`value ^= value >> 27n` on bigint in string-key-hash.ts (SC1043), while the
plain `^` form on the next line compiles. Spell both as `value = value ^
(...)`. The hash is unchanged.
2026-10-08 15:40:53 -05:00
Malte Ubl f7ac13e87f Cache string Map key hashes in the string header
On 64-bit little-endian targets ScrStr's capacity word is split into a
32-bit cap and a 32-bit cached Map key hash (header stays 24 bytes).
Heap strings fill the hash on first lookup; every in-place mutation path
(unique concat, split scratch reuse, regrow, spare reuse) clears it.
Immortal strings are never written: compiler literals and the runtime's
one-character/empty statics carry the hash precomputed (string-key-hash.ts
mirrors scr_key_hash; both sides pin the same vectors).

scr_key_hash seeds by length multiplicatively: XORing the length into the
low bits cancelled against the last byte ("ab" and "abc" collided).

Map string lookups inline the probe into each getter and reject linear
entries by hash; set specializes its probe/overwrite by lookup kind with a
shared append path.

Tests: runtime map.test.ts (pinned vectors, statics, in-place append
invalidation), string-key-hash.test.ts, corpus 3141-map-string-key-hash
(plain + sanitized; mutation-checked against both invalidation paths).
2026-10-08 15:40:53 -05:00
Malte Ubl 1de761c330 Merge pull request #757 from vercel-labs/perf/rebased/15-h20-callbacks
Call array-method callbacks directly and pass plain elements (functional-pipeline 2x faster)
2026-10-08 15:40:49 -05:00
Malte Ubl 520dc20711 Record preflight/order baselines for corpus 4140, 4141, 4142 2026-10-08 15:38:45 -05:00
Malte Ubl ea8f93a99f Keep data-intrinsic operand checks inside the self-hosted subset
The bootstrap-seed build rejected `"receiver" in expr` / `"args" in expr`
on the IR expression union in constant-callbacks.ts (SC1090: `in` on an
object union). A switch over the six data-intrinsic kinds now returns their
operands (receiver first; none for mapNew/setNew, as before) and replaces
the DATA_INTRINSICS set. Behavior is unchanged.
2026-10-08 15:38:45 -05:00
Malte Ubl 425386b877 Call array-method callbacks directly and pass plain elements
Array HOF helpers (%arr.map/filter/forEach/flatMap/find*/some/every/
reduce*) called every callback through its closure, bracketed by the
ambient JS receiver stack, with each element boxed as T | undefined.

- Intern a helper per literal callback and admit %arr.* helpers as
  constant-callback consumers: the callback becomes a guarded direct
  call that LLVM inlines into the loop.
- Replace the scalar-only receiver check with an exact module fixpoint
  (AmbientReceiverReaders) over direct calls, constructors and virtual
  dispatch; known callbacks that cannot reach a receiver read skip the
  push/pop.
- For inline arrows over plain element types, lower the callback a
  second time with the element type and split each visit on the array
  state: present values take the fast closure with no union box; holes
  and present undefined keep the original closure and undefined arm.
- Resolve factory-made callbacks (every return yields one function) and
  immutable captured callbacks at their closure creation sites.
- Read and write scalar capture boxes in place instead of calling
  scr_box_get_f64/scr_box_set_*.

Corpus 4140-4142 pin the split visit, receiver brackets on the direct
path, and factory/captured callbacks.

Linux x64 (sandbox, --layouts=4 --runs=16, baseline stack 06 tip):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 192.5 | 193.8 | +0.7% | -2.1% .. +2.4% | neutral | +0.0% | 436.3 | 0.44x |
| regex-logs | 166.4 | 165.5 | -0.5% | -1.8% .. +0.9% | neutral | +0.0% | 243.9 | 0.68x |
| ast-interp | 264.9 | 243.0 | -8.3% | -9.5% .. -6.9% | faster | +0.0% | 232.2 | 1.05x |
| records-sort | 593.2 | 576.4 | -2.8% | -6.6% .. +0.3% | neutral | +3.0% | 210.3 | 2.74x |
| template-render | 69.3 | 68.6 | -1.0% | -3.2% .. +2.6% | neutral | +0.0% | 189.0 | 0.36x |
| functional-pipeline | 215.8 | 111.0 | -48.6% | -49.6% .. -46.3% | faster | +3.8% | 255.8 | 0.43x |
| async-pipeline | 194.8 | 196.8 | +1.0% | -1.1% .. +12.0% | neutral | +0.0% | 85.9 | 2.29x |
| word-graph | 271.8 | 270.1 | -0.6% | -7.0% .. +5.5% | neutral | +3.0% | 517.1 | 0.52x |
| alloc-trees | 314.4 | 316.6 | +0.7% | -0.4% .. +1.9% | neutral | +0.0% | 124.9 | 2.53x |
| numeric-kernels | 46.2 | 45.7 | -1.1% | -2.5% .. +1.5% | neutral | +0.0% | 128.2 | 0.36x |
| validate-errors | 179.7 | 174.5 | -2.9% | -5.4% .. +0.7% | neutral | +0.0% | 711.1 | 0.24x |
| csv-numbers | 343.0 | 338.9 | -1.2% | -7.3% .. +0.7% | neutral | +0.0% | 519.6 | 0.65x |
| log-summary | 166.1 | 168.3 | +1.3% | -4.4% .. +9.1% | neutral | +0.0% | 300.0 | 0.56x |
| inventory-report | 298.1 | 290.1 | -2.7% | -14.6% .. +5.4% | neutral | +1.3% | 421.4 | 0.69x |

Geomean -5.9%; candidate/node 0.715x. macOS arm64 (shared host):
geomean -4.7%, functional-pipeline -44.1%, ast-interp -11.0%,
inventory-report -8.0%; none slower (records-sort -0.5% on a calmer
rerun).
2026-10-08 15:38:45 -05:00
Malte Ubl b1db834fe1 Merge pull request #756 from vercel-labs/perf/rebased/14-h41-inline-alloc
Inline the small-object allocator into emitted constructors (alloc-trees up to -16%)
2026-10-08 15:38:41 -05:00
Malte Ubl c0e2d097eb Update self-hosting emission cases for the inline allocator
The full emitter inlines the small-object allocator on 64-bit host targets,
so the self-hosting layout fixture's host enables inlineAlloc for 64-bit
requests (as emitLlvmModule does), and the recursive-records case no longer
asserts the width-specific free path (64-bit pushes onto the allocator's
free list; 32-bit calls scr_cyc_free).

(Split from the stack's later "Update self-hosting emission cases for
acyclic proofs and inline allocation"; the acyclic-proof half moves to the
construction-only acyclic change.)
2026-10-08 15:36:07 -05:00
Malte Ubl 88dfc2ebde Record preflight/order baselines for corpus 4410 2026-10-08 15:36:07 -05:00
Malte Ubl 6d1257ba72 Format alloc.ts 2026-10-08 15:36:07 -05:00
Malte Ubl cabfbe1f14 Keep inline allocation out of worker programs
Upstream native workers (095b6193) compile the runtime with -DSCR_WORKERS,
which makes scr_cyc_live and scr_weak_dispose_hook thread-local, and the
small-object allocator is compiled out there. The inline allocation paths
address those as ordinary globals, so worker programs keep the out-of-line
runtime calls, exactly like thread-instanced libraries.

A worker-threads test pins that worker LLVM references neither the
allocator state nor the thread-local counters, and that worker programs
keep the fiber path for async bodies (the entry 07 adaptation).
2026-10-08 15:36:07 -05:00