ThreadSlots<K, V> holds one value per key and thread, like a WeakMap that
each thread has its own copy of; under Node.js it is a WeakMap per name
and thread. Setting a value never writes the key, so values can be set on
published keys.
Classes used as keys carry a 32-bit slot word after their header words,
assigned on the first set for a key (with a compare-and-swap for a
published key) and never reused. Each family has one block per thread
that maps slot indices to values through pages of 1024 entries. get and
has are inline reads of the slot word, the page and the entry; set and
delete call scr_slots.c, which holds one reference to each value. A key
class's free paths release the key's values, and thread exit releases the
rest.
Frame storage for non-escaping records was limited to number, boolean and
inline-union fields. Records with string, array, record or class fields,
tuples, and same-shape spread updates (`{ ...state, count }`) were always
heap-allocated, even when every use only projected them.
Closed record and tuple shapes now qualify regardless of field types. The
frame owns the reference fields: they start empty in entry-addressed slots
and are registered before anything is stored, with the binding's lexical
scope for locals and with the statement frame for call-argument
temporaries, so an exception while the record is being built releases
exactly what was stored. Interned string literal fields need no owner.
A reassigned binding builds the new value apart, copies it over its slot
and then releases the replaced fields. Result-slot entries cover producers
returning such records, tuples and spreads of a binding, and store into
the zeroed caller storage without releasing an old value. A spread of a
binding is a projection for the lifetime analysis. Published shapes and
switch-case bindings of reference-owning shapes keep heap storage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Atomics operations on 8-, 16- and 32-bit integer arrays are sequentially
consistent atomic instructions on the element, and no longer take the
process-wide shared-memory lock. Other reads and writes of shared storage
are relaxed atomic accesses: element reads and writes access the element,
and copies, fills, conversions, searches and sorts read shared bytes into
a private copy and write back only the range they change, so concurrent
accesses to the same bytes are free of data races. Non-shared storage
keeps its plain memory accesses. Atomics.wait and Atomics.notify queue
waiters in buckets chosen by location, each with its own lock and
condition variable, so waiting threads no longer serialize other Atomics.
Atomics.store answers +0 for a value that truncates to -0, as Node does.
The shared storage runtime test also runs under ThreadSanitizer.
Every region creation, release and pressure retry walked every condemned
region again. A released region that something still refers to was
re-walked on each of them, two or three times per build in a watch loop,
and each walk visits every object of the region.
An attempt that keeps a region now records the references counted to it.
Retries at creation, release and region entry attempt a kept region again
only when that count has changed, or once the thread has allocated 8 MiB
(or the region's size, if larger) in regions since the attempt. That
volume doubles after each attempt that finds the count unchanged, up to
64 times. The volume covers references from ordinary objects the walk
reaches, which can go away without changing the count. The schedule is
per region, so a region that another region's release unblocks is
attempted at once, and the full cycle collection before an attempt runs
only when some region is due.
Adds the region-builds runtime benchmark and a region test for the
schedule.
publish() used to remove each published object from this thread's
cycle-collector candidate buffers after the walk, which loaded every
object's collector header a second time. On graphs larger than the
cache that second pass took about a third of the publish.
scr_pub_mark now takes a buffered object out of its buffer when it marks
it, while the header is in cache: the slot is cleared and its index
recorded. When the publish ends, scr_cyc_pub_fill closes the empty slots
with entries from the ends of the buffers. A refused publish puts each
taken object back in its slot first. After the walk, the log is read only
for dyn containers, the RC audit and LeakSanitizer.
The emitted walkers test null and immortal references (parent links,
values published before) before calling scr_pub_push, and the string
walker checks for ASCII 32 bytes at a time.
Adds the published-graphs runtime benchmark, a cycle collector test for
the take, restore and fill hooks, and corpus program 5702.
@scriptc/experimental exports InternTable<K, V>: a named table that every
thread of a scriptc program shares and that each thread has its own copy
of under Node.js. Keys are strings or numbers with Map equality; values are
published (@scriptc/threads) when stored, and the first stored value of a
key is kept. Members are get, has, size, getOrInsert and
getOrInsertComputed.
The runtime table (scr_itable.c) splits each table into 64 segments.
Lookups take no lock and do not write; inserts take a segment's spin lock
for a probe and a few stores, and callbacks run without a lock. Number
tables keep keys and values in the index; string tables keep entries in
segment-owned chunks behind tagged slots. Replaced indexes are kept.
The compiler lowers the class to a synthetic class with one f64 handle
field and five intrinsics, with interned helpers per member and type and
an in-place lowering of getOrInsertComputed that creates a function
expression callback only on a miss. Stored values go through the publish
intrinsic, so their types get the write guard. scriptc refuses names that
are not string literals, one name opened with different type arguments,
and key or value types a table cannot store.
scr_publish walks small graphs in inline buffers and reads
SCRIPTC_PUBLISH_TRACE once, instead of allocating and calling getenv on
every call.
A candidate release now also runs a pass once the live cycle-headered
count has grown by 4096 objects since the last pass. A dead graph is
usually reachable from one candidate (its root), so the candidate count
alone let a few hundred dead graphs pile up before a pass, which then
walked them after they had left the cache.
markGray records the objects it grays and counts the ones still
externally referenced after the trial decrements. When none is, every
gray object is garbage, so the pass whitens the recorded set and skips
scan. markGray also records each edge it skips into an older generation,
so a restricted pass pins those targets without tracing the white set a
second time.
Adds the cyclic-graphs runtime benchmark and corpus programs 5700 and
5701.
Number-key reads of a map without the direct index called
scr_map_entry_f64 for every lookup. The internal sc_map_entry_f64 helper
now calls sc_map_entry_f64_hashed for such maps instead: it hashes the
key as scr_map_find_f64 does (SameValueZero key word, scr_key_mix, the
absent-key filter), scans the entries of a small map or probes a compact
bucket table, and calls the runtime lookup only for a table wider than
the compact limit. It stays out of line so the direct-index path stays
small at every call site.
The runtime fallback is called as a read-only call, so both helpers only
read memory and LLVM merges lookups of one key when nothing is stored in
between, such as a has() followed by get().
The runtime benchmark suite gains number-maps: a node table indexed by
dense ids, scope tables keyed by sparse ids, and get/has/set mixes.
In worker-enabled executables a typed-array access tests whether the view
is shared. The test used to come before the index check, and the shared
path called scr_bytes_get_or_nan/scr_bytes_set with the index as a
double, then converted the element back to an integer. Both paths now
use the index the inline check produced: the shared test follows the
check (a view's length never changes), and the shared path calls new
runtime entries that take the index as a size_t
(scr_bytes_shared_get_int, scr_bytes_shared_get_f64,
scr_bytes_shared_set). Integer elements join as one 32-bit value and
convert to a number after the join, so compares and ToInt32/ToUint32
of the result stay integer operations in every element kind.
The amortized worker cancellation poll moves from the loop header to
the loop's back edge (the continue target). In the header it was a loop
exit ahead of the body, which kept LLVM from rotating the loop and left
loads such as the view's length inside it.
Adds the typed-array-scan and typed-array-scan-workers runtime
benchmarks: Uint8Array, Uint16Array, Int32Array and Float64Array scan
loops in an ordinary and a worker-enabled executable.
A record built from unchecked element reads can hold undefined in a field
the checker calls present. Such a record kept its widened layout only in
an inferred binding: an annotated parameter or return, or an unannotated
return, converted it to the declared layout with a checked narrow, which
throws a TypeError for a missing element where Node passes undefined
through.
Record values that flow between bindings and the parameters and results
of functions and methods whose every call is visible now form flow
classes. A class takes the widened layout, with the union of its members'
optional fields, when it is closed: each value entering it is a fresh
object literal or comes from a member, each use of a member reads or
writes its fields, passes it to another member or only inspects it, and
every member declares the same shape. Members then share one layout, so
no conversion runs at the boundary and the record keeps its identity.
A class that reaches other storage keeps the declared layout for every
member, including the inferred bindings and the record parameters the
analysis widened before. Converting there copied the object, so writes
through a parameter or a pushed record did not reach the caller's object;
such a literal now converts once where it is created.
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.
The suite only had single-process CPU workloads. Add the two largest
real-world categories:
- http-api (kind "server"): a node:http JSON API in the static subset
(routing, query parsing, seeded catalog, per-cart state, typed JSON
bodies, ETag/If-None-Match, auth and request-id headers), driven by
clients/http-load.mjs, a raw-socket keep-alive HTTP/1.1 load client
that is identical for every server. 64 connections, 2 client threads,
4,000 untimed warmup and 40,000 measured requests on loopback. The
per-connection request schedule is seeded and server state is
partitioned per connection, so the response digest is deterministic.
Correctness compares server stdout, server stderr (minus the PORT
line), exit status, and client stdout with Node byte-for-byte. The
timed sample is the measured-phase wall time; a second table reports
req/s, p50/p90/p99 latency, client CPU, and peak RSS.
- cli-config (process, launches: 20): a deploy-plan CLI that reads a
~110 KB JSON manifest, validates, interpolates env values, and orders
80 services into dependency waves. Each sample is the median of 20
launches, so startup dominates.
The runner stays backward compatible: process workloads behave as
before, the JSON report keeps its fields and adds kind, launches,
category, server, node_peak_rss_bytes, and geomean_vs_node_by_category,
and the summary prints per-category geomeans (cpu, startup, server).
Fourteen application-shaped workloads (JSON, regex, parsing and class
dispatch, sorting, rendering, collection pipelines, async fan-out,
Map/Set graphs, allocation, numeric kernels, exceptions, CSV, and the
two multi-module build benchmark apps on large inputs).
bench:runtime builds each workload with one or two checkouts, refuses to
time any executable whose stdout, stderr, or exit status differs from
Node, interleaves baseline and candidate launches, and reports medians
with bootstrap 95% confidence intervals, binary size, and peak RSS.