Commit Graph
9 Commits
Author SHA1 Message Date
Chris Tate 8e507160c4 Add BroadcastChannel and the remaining Node 24 worker_threads surfaces 2026-10-17 11:06:47 -05:00
Chris Tate d3dda8271d Leave the main thread's heap to the operating system at exit
When the main thread's script ends, the executable released every module
global and ran a full cycle collection before exiting, which takes time
proportional to everything the program still holds (about 15 ms for
1.5 million live objects) and frees memory the operating system reclaims
anyway. The main thread now skips those releases, the exit-time
collection and the collector's buffer cleanup. Exit listeners and output
flushing run as before. Worker threads still free their heaps when they
exit, since later threads reuse their arenas, and builds that check for
leaks (the RC audit, AddressSanitizer) free everything as before, as does
any process run with SCRIPTC_EXIT_FREE=1 for external leak checkers.

SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it
took to start, to run its script and to exit.
2026-10-16 01:58:45 -05:00
Malte Ubl a8c9b45a0a Read the macOS worker stack guard from a thread-specific-data slot
In worker executables scr_stack_guard is thread-local, and on Darwin every
thread-local access calls the variable's TLV thunk. Every stack-checked
function paid that call on entry; in tsc-ts the checks were about 12% of
the instructions retired on macOS.

The runtime now also keeps the running stack's guard in a pthread key's
thread-specific-data slot, written wherever scr_stack_guard changes
(thread start, fiber switches). libpthread stores a thread's values in an
array at its thread pointer, indexed by key; scr_stack_guard_init probes
that layout once and exports the slot's byte offset. Emitted checks in
Darwin worker executables load the guard from TPIDRRO_EL0 (masked) plus
the offset on arm64, or %gs plus the offset on x86-64: no call. If the
probe fails the offset stays zero and the checks call
scr_stack_guard_get from a cold block. Other targets are unchanged.
2026-10-11 06:39:43 -07:00
Malte Ubl bdba4a7a1c Test an exact per-thread pending word in ELF worker executables
Worker programs on Linux tested a process-wide alert word after every
call that may throw and called scr_exc_pending on its slow path to answer
for the calling thread. Each of the ~55k slow-path calls in tsc-ts counted
against LLVM's inline cost, and while any thread had an exception in
flight every other thread took the slow path.

ELF worker executables (the emitter's executableTls targets) now give
every script thread an exact pending word in the executable's static TLS
block, and the emitted check is one thread-pointer-relative load and a
branch to the unwind, with no out-of-line call:

- bit 0 mirrors "active cell kind != NONE". Only the owner thread writes
  it, at every change of its active cell (throws, rethrows, take/clear,
  fiber switches, payload moves, the termination sentinel). The uncaught
  printer now settles it after resetting the cell.
- bit 1 requests the context's stop. worker.terminate() sets it through a
  pointer the worker registers under a mutex while its script runs (a
  terminate before registration is found through the cancel flag), and
  scr_context_stop sets it for process.exit() and fatal paths. The stop
  bit counts as the stop signal in scr_context_quiet/checkpoint.

A set stop bit unwinds while the active cell may still be clear, so every
consumer of an unwind installs the TERMINATE sentinel before inspecting
the cell: emitted try regions test the bit before entering a handler and
call scr_context_checkpoint, and fiber completion, the uncaught handlers
and exit listeners checkpoint instead of only testing
scr_context_stopping. The runtime's out-of-line scr_exc_pending already
checkpoints. Darwin and Windows keep the alert word (a Darwin thread-local
access is a call).

New corpus programs pin terminate() inside catch and finally bodies, with
exceptions stashed or propagating, inside generator and async bodies and
runtime callbacks, and before the worker starts, plus every way an
exception is raised, moved, consumed or discarded on the main thread and
in a worker, each followed by a may-throw call that must not unwind.
2026-10-11 06:39:42 -07:00
Malte Ubl 03ae137be2 Test a process-wide exception alert in worker executables' polls
Every emitted pending check of a worker executable loaded the
thread-local active exception cell and the thread-local stop signal.
On Darwin each thread-local access is a call (_tlv_get_addr), and LLVM
does not share them across a function's checks: compareTypes made 61
such calls, and the single-threaded tsc-ts self-check spent 19% of its
time in them (a Worker build ran 1.67x slower than the same program
without Worker on macOS).

The checks now load one ordinary global, scr_exc_alert, which is
nonzero while any script thread may have an exception pending in its
active cell or must stop. Zero proves nothing is pending anywhere, so
the check falls through; otherwise the out-of-line scr_exc_pending
answers for the calling thread, observes termination and settles the
thread's share. Each thread holds at most one share: raised by throws,
rethrows, the termination sentinel, fiber switches and payload moves
into the active cell, dropped by take, clear, moves out of the active
cell and the slow path. A share is only changed by its own thread, so a
thread's raise is visible to all of its later checks. worker.terminate()
holds one more reference until the worker's script has ended, so the
worker reaches its slow path and stops. An exception in flight in one
thread costs the others slow-path calls only until it is caught.

The stop signal is runtime-private again; the active cell stays
exported for ordinary executables' inline checks.

tsc-ts self-check, macOS, Worker build vs non-worker build: 1.67x ->
1.18x (check phase 1.65x -> 1.09x).
2026-10-09 17:43:50 -07:00
Chris Tate 3bcf761f42 Throw a catchable RangeError on deep recursion
Native executables used to terminate with a signal when recursion
exhausted the stack. Functions that can take part in unbounded recursion
now compare the stack pointer against a per-stack guard on entry and
throw Node's `RangeError: Maximum call stack size exceeded` through the
ordinary exception path, so catch handlers, finally blocks, worker error
events and the uncaught-error exit code behave as they do under Node.

- The checked set is every function on a cycle of the call graph, where
  operations that can reenter compiled code indirectly (indirect calls,
  callbacks, dynamic and library operations) reach closure targets and
  class members. Non-recursive functions, including leaf callbacks, stay
  check-free, and the may-throw analysis accounts for the new throw.
- The runtime installs the guard per context (main thread from the stack
  rlimit, workers from their thread attributes) and per async fiber, with
  a reserve for the throw, unwinding and error reporting. Worker threads
  now get an explicit 8 MiB stack.
- Scalar call results pass through the unwind path as its dummy value, so
  calls in tail position keep tail-call and recursion elimination.
2026-10-09 16:55:35 -05:00
Chris Tate 96ee89412b Keep exception polls inline in worker programs (#785) 2026-10-09 10:01:27 -05:00
Chris Tate 1f10e83720 Promote canary to main (#738)
- Preserve callback identity, receiver binding, and class references across unions and collections.
- Track record field presence independently of undefined values and improve native runtime behavior.
- Expand differential coverage and streamline CI bootstrap and compiler rebuild checks.
2026-10-08 11:00:13 -05:00
Chris Tate 095b619330 Add native workers and shared memory
- Add native worker execution with isolated module state, structured messages, and lifecycle controls.
- Support shared buffers and integer atomics with coordinated runtime ownership and cancellation.
- Extend compiler, runtime-pack, differential, and compatibility coverage for native concurrency.
2026-10-07 19:07:02 -05:00