Skip to content

Free the address space after process.exit off the critical path (--detached-exit) - #860

Merged
ctate merged 4 commits into
mainfrom
perf/detached-exit
Oct 11, 2026
Merged

ctate merged 4 commits into
mainfrom
perf/detached-exit

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Branch perf/detached-exit, stacked on perf/worker-fd-table (open after it). One commit.

process.exit ends in _Exit. The kernel then frees the whole address space before the parent sees the exit: 30–40 ms per GB of 4 KiB pages, about 2.5 ms/GB with THP. For tsc-ts that is 21 ms (tRPC), 46 ms (TypeORM) and 66–110 ms (Playwright) after its last instruction.

With scriptc build --detached-exit (also scriptc run), the program's process.exit on Linux first clones a helper that:

  • shares the address space (CLONE_VM);
  • blocks all signals;
  • closes every descriptor except the read end of a pipe;
  • waits for EOF on that pipe.

The kernel releases an exiting process's memory (exit_mm) before its files (exit_files). So EOF means the helper holds the last reference. The helper then exits, and the pages are freed there, after the waiting parent has already seen the exit status. This is the latency mold hides with --fork. The program's output, exit status and descriptors are unchanged: the helper holds no descriptor the program had.

Why a build-time option

The helper keeps the executable mapped for up to ~100 ms after exit (overwriting the binary in place then fails with ETXTBSY), the memory stays charged to the cgroup for that long, and the teardown CPU time no longer appears in the parent's wait4 rusage. That has to be the program author's choice, so it is opt-in at build time instead of an environment variable read at run time.

How it flows

  • CLI: --detached-exit for build/run, executables only (--emit other than exe, coverage, --lib and cache warm reject it). Documented in the CLI help and on the CLI docs page.
  • CompileBaseOptions.detachedExit → the LLVM emitter defines @scr_detached_exit = constant i8 1 in the program object. The runtime (scr_lib.c, Linux) declares scr_detached_exit as a weak constant 0 and reads it in process.exit. The program's strong definition overrides it at link time, so there is no new runtime flavor and no linker flag; builds without the option are unchanged.
  • Caches: the option is part of the early executable cache key and route key (including the CLI bootstrap fast path); the native compiler's frontend cache already keys on the whole options object.
  • No effect on other platforms (the runtime only reads it on Linux).

Measurements (overnight, with the environment-variable version; tsc-ts opted in; not re-measured)

Interleaved, median of 12, same tsc-ts with and without the opt-in:

without with
8 vCPU tRPC 0.380 s 0.360 s (−5.3%)
8 vCPU TypeORM 0.990 s 0.965 s (−2.5%)
8 vCPU Playwright 1.390 s 1.330 s (−4.3%)
16 vCPU tRPC 0.550 s 0.500 s (−9.1%)
16 vCPU TypeORM 0.915 s 0.855 s (−6.6%)
16 vCPU Playwright 1.400 s 1.290 s (−7.9%)

Tests

  • Unit: emitter.test.ts (the definition appears only with the option, and nothing else in the module changes), executable-cache.test.ts (the option isolates cache entries), output-options.test.ts (non-exe emits reject it), source-output.test.ts (non-executable compile output refuses it): 4 files, 95 tests pass.
  • macOS: building the same program alternately with and without the flag gives the definition only with it (the cache never crosses); both binaries run with identical output and exit code.
  • Linux sandbox: a program that allocates and calls process.exit(3), built without / with the flag: nm shows the runtime's weak V scr_detached_exit / the program's R scr_detached_exit; strace -f -e clone shows no clone / one clone(CLONE_VM|SIGCHLD) whose child exits after the parent; same for --optimization=speed and --sanitize. Output and exit code identical.
  • Linux: scr_worker.test.ts + scr_alloc.test.ts 5/5; differential corpus -t "worker|exit" 54/54.
  • Self-hosting check (bootstrap.js build packages/compiler/src/native/cli.ts --emit=ir): no diagnostics. pnpm lint, oxfmt --check on touched files: clean. Docs: cd docs && NEXT_DIST_DIR=.next-check pnpm check passes (23/23 route tests).
  • Full gates not run.

…tached-exit)

`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.
@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 11, 2026 5:18pm UTC

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.
Base automatically changed from perf/worker-fd-table to main October 11, 2026 17:08
@ctate
ctate merged commit 60d5b25 into main Oct 11, 2026
19 of 20 checks passed
@ctate
ctate deleted the perf/detached-exit branch October 11, 2026 17:19

This branch was successfully deployed

1 active deployment
Preview — 00f88680 Deployed Oct 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants