A fully typed net server, HTTP server or child-process program linked the dynamic object model: about 290 KB of a 616 KB HTTP server. Three kinds of reference held it, and none was the program's doing. This cuts them. 117 corpus programs now link no dynamic-tier symbol at all, and none gains one.
What held it
Traced with lld's --why-live and a pass over the live relocations of each link (one chain per symbol hides every root but one).
- The emitted
maininstalled the handle ops of every unit the program links (scr_net_dyn_install,scr_http_dyn_install,scr_child_dyn_install). The ops tables name every*_dynh_*function. scr_error_set_propbuilt the error's dynamic node at the throw site. A connect error'saddressandport, an exec error'sstatus,outputandpid: each call ranscr_dyn_from_error, so any program that could raise such an error linked the node builder and everything behind it. With the installs gone this alone kept 185 KB.- Typed paths that named a dynamic entry directly: the request settle path called a
signaloption's disposer throughscr_dyn_call; the accept path built the'drop'event's data object;setHeaderwith a typed string went through the dynamic coercion; a request without an agent boxed the global agent; the zero-argument write callback shared a binder with the dynamic one.
What changed
- Units install their handle ops where a handle is first boxed.
scr_net_socket_to_dyn,scr_net_server_to_dyn,scr_http_req_to_dyn,scr_http_res_to_dyn,scr_http_client_to_dynandscr_child_to_dyninstall on first use, the compiler's to-dyn helpers call them (DYN_HANDLE_BOX_FN, the mechanismscr_dgram_to_dynalready uses), and every internal box goes through them. The linker keeps a unit's dynamic members exactly when linked code can box one of its handles. In a program that links the HTTP unit, net handles box throughscr_http_server_to_dyn/scr_http_socket_to_dyn, so a boxed HTTP server always has its'request'hook. - The receiver is the one box no unit sees coming: a typed fire pushes
(pointer, tag)andscr_dyn_this_getboxes by tag when dynamic code readsthis. New runtime featuredynReceiver(the module hasdyn.thisordyn.generatorThis, or converts a dynamic value to a function type);maininstalls before%mainonly then. Entry points that take a dynamic listener, and the stream bridge that reads a child's stdio through its dynamic members, install too. - An error's attached properties wait for its node.
scr_error_set_proprecords(key, value);scr_dyn_from_errorapplies the recorded ones when it builds the node, in the throw site's order. A recordedcodekeeps its position (sites that stamp the code slot and then setcodeafterlibraryandreasonrely on it).'key' in errfrom typed code answers from the record without building the node;util.inspectbuilds it. Every call site keeps its call;scr_child_error_selfuses the newscr_error_set_prop_self. - The typed paths go through a pointer their dynamic entry sets (
scr_http_client_add_abort_signal,scr_net_server_on_drop) or a typed helper (scr_http_validate_header_text,scr_http_request_global). The pointers have external linkage: the compiler folds a static one with a single stored value back into a direct call.
The new functions in scr_json.c sit at the end of the file and are named in the middle only through a constant table, so the unit's existing functions are emitted in their existing order and programs that link none of this keep their bytes.
Behaviour
No change, with one exception that moves towards Node: an error's node is built at the first dynamic read instead of at the throw site, so typed code that assigns err.message before any dynamic read now sees the new message through any as well (the node used to hold the throw site's message). Pinned in 8896-net-error-properties.ts.
New corpus programs, differential against Node in both lanes:
8885-net-typed-server-echo.ts,8886-http-typed-server.ts,8898-child-typed-exec.ts: fully typed.tests/harness/corpus-dynamic-tier.test.tsholds them to zero dynamic-tier symbols, asbench-dynamic-tier.test.tsholds the benchmarks.8887-net-handles-reach-unknown.ts,8888-http-handles-reach-unknown.ts: typed handles that reachunknowncode late, after typed listeners were registered, and a dynamic'request'listener on a typed HTTP server.8889-js-net-listener-receiver.js: listeners whose only way to the handle isthis.8896-net-error-properties.ts,8897-child-error-properties.ts: the attached properties throughany,in,Object.keysandJSON.stringify, in Node's order.
Numbers (Linux x86-64, release)
| Program | Before | After | Dynamic-tier symbols after |
|---|---|---|---|
typed HTTP server, 7 lines (createServer, listen) | 603,792 | 322,960 | 0 |
| typed net server and client | 485,776 | 255,368 | 0 |
typed execFileSync CLI | 373,840 | 148,560 | 0 |
corpus/2982-http-static-close-all.ts | 616,120 | 325,904 | 0 |
corpus/1470-child-lifecycle.ts | 437,576 | 225,608 | 0 |
corpus/1462-exec-sync.ts | 395,216 | 179,352 | 0 |
bench/http-api | 848,824 | 747,856 | 33 (typed JSON.parse, not part of this change) |
The first three were measured on 714482d49 against this branch; the rest are gate rows against e8c933e5d.
Zero-cost gate, --corpus=all, PASS, with --allow for sources that import net, http, https, http2, tls, child_process or assert (and the Intl programs, whose ICU check needs the pattern when their bytes change): 5,350 programs, 2,379 identical, 463 allowed, 2,405 runtime-only, 0 differ.
- 292 executables shrank: median 102 KB, 149 of them by more than 100 KB, 35.6 MB in all. 117 programs (68 child-process, 43 HTTP, 6 net) dropped every dynamic-tier symbol. No program gained one.
- 2,575 executables grew, every one a program that links dynamic-tier symbols before and after: median +256 bytes, 90th percentile +376 bytes, largest +4,496 bytes (0.42%, a dynamic HTTP program). This is the cost of the second change:
scr_dyn_from_errortests whether a recorded property waits for the node it builds, and the dynamic HTTP and net programs also take the box functions. A dynamic program that builds no error node is byte-identical. - Benchmarks: 42 of 58 byte-identical, including every fully typed one. 15 dynamic workloads +192 to +320 bytes.
bench-runtime: 65 workloads, 64 neutral, geomean -0.3%; the one outside tolerance (optional-fields+1.7% [+0.2%, +10.2%]) is a typed workload whose executable is byte-identical. - Full lanes in Sandboxes: plain 21,601 passed, sanitized 18,368 passed; the failures are test timeouts (three.js, an Effect pilot, two Test262 compile timeouts, one npm case that passes when rerun alone, a vendor-cache test and a
beforeAllcompile). Plain and sanitized native bootstraps pass. DYNAMIC_WORKLOADSis unchanged:bench/http-apiis the one workload this change touches, and typedJSON.parsestill holds it.
🤖 Generated with Claude Code
Activity
5Brought up to date with main (3a512b320; the head also merges cleanly with 50bcfe1b3): merge commit 39abceaef, and one commit the merge needed to build, 61eaf41c2.
What conflicted.
backend/llvm/emit-module.ts: #3232 installs the builder of a failed spawn's error object (scr_child_error_event_install) for itschildErrorEventfact, next to the child handle ops install this branch moved from everychild_processprogram to the ones that read the dynamic receiver or add handle properties. The handle ops keep this branch's condition; the error-event install ismain's, unchanged.runtime/src/scr_child.handir/runtime-features.test.ts: both sides' declarations and tests, kept.- Not a textual conflict, found by the build (
61eaf41c2): #3228 gavescr_http_request_pooleda parameter that says the header pairs are anhttp.requestoptions record's.scr_http_request_global, this branch's typed path for a request that names no agent, called it without one. It now passes that they are, which is whatmain's null-agent arm does through the same function, so an agentless request's host and headers are checked before a socket is taken, as onmain(#3228's8854-http-request-option-header-checkspasses).
How the two changes compose. #3232's builder sets a failed spawn's fields through scr_error_set_prop, which on this branch records them until the error's node is built; #3232's programs (8722-child-spawn-error-fields, 8730-child-spawn-error-typed) and this branch's 8897-child-error-properties pass together, so the recorded order is Node's. No program gains a dynamic-tier symbol: tests/harness/corpus-dynamic-tier.test.ts and bench-dynamic-tier.test.ts pass, and the gate reports none.
Re-run on the pushed head 61eaf41c2 (Vercel Sandboxes; the tests on 32 vCPU, Intel Xeon @ 2.90GHz):
- Workspace build with the runtime pack rebuilt,
pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only: pass. - Zero-cost gate,
--gate-corpus=all, the allow pattern of the description, against3a512b320(which contains #3199): 5,388 programs, 2,392 identical, 474 allowed, 2,419 runtime-only, 103 fail to build on both sides, 0 differ. Before the merge, againste8c933e5d: 2,379, 463, 2,405, 103, 0.- 300 executables shrink (292 before): median 102 KB, 152 by more than 100 KB, 36.0 MB in all. 119 programs drop every dynamic-tier symbol (117 before); none gains one.
- 2,592 grow (2,575 before), each a program that links dynamic-tier symbols before and after: median +256 bytes, 90th percentile +384, largest +4,576 (
8854-http-request-option-header-checks, +0.24%). - 43 of 59 benchmarks byte-identical, every fully typed one among them;
bench/http-api849,888 to 748,920 bytes. - Compile time: benchmarks -0.32% [95% CI -0.69%, +0.61%] and fixtures -0.44% [-1.55%, +0.66%] within tolerance; self -0.75% [-4.67%, +3.11%], which the gate calls inconclusive.
- I ran it without
--gate-max-size-growth, so its printed verdict is FAIL for the 173 allowed programs that grow; there is no other failure line.
- bench-runtime, two rounds of 66 workloads: geomean +0.1% with 63 neutral, then -0.3% with 64 neutral. The workloads outside tolerance differ between the rounds:
dynamic-properties(a dynamic workload, +256 bytes) measured +4.2% [+1.3%, +12.4%] and then +0.4%;string-iteration+1.9%,n-body-5.1% andvalue-objects+2.0% are byte-identical executables;intern-table-mapmeasured -8.5% in the second round only. - Differential and LLVM differential tests over every corpus program matching
child|spawn|exec-file|execfile|exec-sync|http|net-|socket|tls|error-prop|error-fields|dyn-handle|8889-js-net|fs-read-directory|require-missing(this PR's eight programs and #3228's, #3232's, #3202's and #3259's among them): 588 pass plain, 588 pass withSCRIPTC_SAN=1. corpus-dynamic-tier.test.ts,bench-dynamic-tier.test.ts,runtime-features.test.ts,link-feature-lists.test.ts,tests/harness/errors.test.ts: 222 pass.
The row for this PR in the gate exceptions list carries the new numbers. Not re-run on the merged head: the native bootstraps and the full lanes (the description's ran before the merge).
CI will show one failure that is main's until #3306 merges: executable-cache.test.ts > "every build and run option is in the cache keys or says why it need not be" (plain Linux lane).
🤖 Generated with Claude Code
The latest updates on your projects. Learn more about Vercel for GitHub.