Response.clone and Request.clone tee the body. The tee branches of a body
read from a JavaScript ReadableStream did not keep that body's chunk check,
so consuming the original or the clone took string chunks as text. The
check is now a flag on the body's stream that its tee branches inherit, and
consuming either branch rejects with Node's TypeError "Received
non-Uint8Array chunk".
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.
- record.isFrozen, array.isFrozen and the extensibility test of Maps and
Sets also read an immortal rc word as published, the test of the
published write guards, for the types the program may publish
(PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
for a published object, array, Map or Set, or a checked view of one,
before materializing the view.
Worker runtimes reserve one region for 32 arenas of 40 class slices. When
the host refused the full region (32 arenas of 1 GiB slices, 1.28 TiB of
address space), the allocator kept 32 arenas and shrank every slice. Under
valgrind, which maps at most 56 GiB, slices dropped to 16 MiB, and every
class whose live blocks outgrew 16 MiB took the system allocator.
Callgrind profiles then counted system allocations that a native run
never makes: tsc-ts checking VS Code made 10.3 M small calloc calls under
valgrind and 0.06 M natively.
The reservation now tries arena counts from 32 down to 4 at each slice
size, from 1 GiB down, before smaller slices, and 2 and 1 arena only when
no slice size fits 4. Arenas that are not reserved stay claimed, so
threads beyond them use the system allocator, and the region bounds of
foreign frees use the arenas reserved. Hosts that map the full region keep
32 arenas of 1 GiB slices.
A runtime test limits RLIMIT_AS to 60 GiB and checks that the first thread
gets 256 MiB slices for 400,000 blocks of 56 bytes, that three more
threads get arenas and a fourth uses the system allocator, and that blocks
freed across threads keep their contents.
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").
instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.
These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
View target structs no longer grow: every stack copy of such a record grew
by the two words, which enlarged unoptimized frames enough for the
sanitized self-rebuild to run out of stack. The guard's cold path looks the
slot up through the view's record-source entry (scr_record_view_slot),
which now holds the viewed record and its offsets, and stack objects of
target types are allowed again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unoptimized builds give every value a frame slot, so the inline guard
grew the frames of large recursive functions enough for the sanitized
self-rebuild to run out of stack. Development builds now call
scr_record_view_field for the slot address; optimized builds test the view
mark as one range (the frozen bit plus a count) before the cold lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view guard's slot lookup moves into scr_record_view_slot, and the
branch to it is weighted as unlikely, so a field access of a view target
type adds one rc-word test to its hot path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value of a view target type can point at another storage (a shared
field read of a shape union, for one), whose block ends before the view
words. The view mark is now the rc word's frozen bit, which no other value
of a view target has (targets are never freezable; an immortal word is no
view), and the trailing words are read only after it. View targets release
like freezable shapes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tagged shape's values share field reads with the other arms of its shape
union, which carry no view words, so tagged shapes keep their copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A checked conversion of a capsule of record A into record type B, when A
has all of B's fields with the same types in any order, now answers a view:
a B object whose two trailing words hold A and A's offsets for B's fields.
Every field address of a B value tests the first word (recordViewFieldPtr,
the record clone and the JSON walker), so reads and writes through either
name reach the same storage. The view's record-source entry owns A's
capsule: boxing the view answers that capsule, so identity holds, B's
reflective surfaces and spreads see the whole object, and the entry dies
with the view, which releases A's capsule. The capsule finds its views
again through borrowed cast-list entries.
Only the target types of such conversions carry the words and the test.
They are planned from the IR before emission (record-views.ts): record
types a checked conversion can produce for which another record type in
the module has all their fields. Programs without checked conversions are
unchanged. This replaces the prefix pointer pass, whose narrower release
could not free the source's extra fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each thread keeps up to 8 MB of arena chunks and up to 64 dropped arenas
for reuse (scr_arena.c). Nothing freed them when a worker thread finished,
so every worker that used a group arena left its pool behind: 40 workers
run one after another, each dropping a 12 MB arena, kept 328 MB resident.
scr_arena_thread_exit frees the calling thread's pooled arenas and chunks.
The worker thread calls it after its context cleanup, before it posts the
exit event. The new harness test runs 24 such workers in sequence and
checks that the resident set grows by less than 32 MiB over the last 20
(160 MiB before this change, 1 MiB after).
In JavaScript sources, ReadableStream, ReadableStreamDefaultReader,
ReadableStreamDefaultController, CountQueuingStrategy and
ByteLengthQueuingStrategy (globals and node:stream/web exports) are class
values over a new runtime unit, scr_webstreams.c, which follows Node 24's
webstreams step by step: underlying sources, queuing strategies, default
readers, tee, ReadableStream.from and async iteration settle their promises
in Node's order. scr_dyn_handle_class.c gives a dyn handle tag a
constructor value with a prototype, brand checks and Node's constructor
errors. Stream state carries cycle headers, so the collector reclaims
streams whose callbacks close over their controllers.
Promise steps run as native promise reactions, a variant of scr_async.c
that only programs with web streams link; every other program links the
default object. TypeScript sources keep their static web stream lowerings.
Byte streams, BYOB readers, pipeTo and pipeThrough throw SC2020.
node24-web-stream-readers and node24-web-stream-readable-lifecycle now
match Node.
The LLVM backend can emit scr_census_write, which a census program
defines and the runtime calls through a weak reference, so the header
declares it as it does scr_detached_exit.
Libraries never build the census (heapCensusOf), but the runtime's weak
scr_census_write reference made library archives list an external
symbol outside their declared set. Declare and call it only in programs.
fork() copies only the calling thread, so a child forked while the
allocator's prefault helper held scr_sa_pf_lock waited on that lock forever
at its first fault-in request, and a child that released the range the helper
was faulting in waited forever for the helper to finish it. Native builds'
emit workers hung this way, with the parent waiting in waitpid.
The helper's first start now registers pthread_atfork handlers. The prepare
handler takes the lock across the fork and the parent handler releases it.
The child handler resets the ring, the busy range, the waiter state and the
heap polling state, reinitializes the lock and both conditions, and marks the
helper not started, so the child's first request starts a helper of its own.
check_fork_while_prefaulting in scr_alloc.test.c forks while the helper
faults in 64 MiB, over 64 rounds; each child releases that range, faults in a
range of its own and must exit within five seconds. ThreadSanitizer builds
skip it.
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.
A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
The constructor took the program object with an empty name for the main
program, which is how glibc reports it. musl reports the main program
with its path, or "/proc/self/exe" in a static executable, so on musl
targets --large-pages found no segment and left the code file-mapped.
The constructor now takes the executable segment that holds its own code:
the runtime links into the executable, so that is the main program's
code under either C library.
scr_text_pages.test.ts runs its check against a static musl build made
with zig as well as the glibc build.
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.
`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.
The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.
Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The static hello binary grew past the 96 KiB ceiling of the runtime
tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does
not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and
hello links to about 65 KB there. The Sandbox gate installs Ubuntu's
clang-22 without lld, so it links with GNU ld.
dd418b067 crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c
defines floor, fmod, frexp and other glibc names as hidden functions so a
program can start without libm. With GNU ld that cannot work: ld decides
--as-needed before section GC, so the runtime's unreachable code keeps
libm needed, and it keeps every regular definition of a name a needed
shared library defines, hidden or not. Every executable carried the whole
unit (about 2.5 KB of code and unwind data) and still loaded libm. Links
without lld now leave the unit out; its functions answer what glibc's do.
192ea3247 installed scr_gen_tag_reader from a load-time constructor in
scr_async.c. A constructor is a link root on every format, so each
executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string
literals (about 1.1 KB). The generator runtime now installs the reader
when it creates a generator, the only time a reader can be needed.
Hello is now 93,928 B with GNU ld and 65,472 B with lld.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template literal of strings and numbers reserved 32 bytes for each
number, the longest number text, and kept that capacity for the result's
lifetime: an 8-byte name built from two numbers used a 96-byte block. The
numbers now format into a stack buffer first, so the result is allocated
once at its exact length.
String.prototype.indexOf took its byte-search fast path on a proven-ASCII
receiver only when the start index was not positive. A positive start went
through the general path: ToIntegerOrInfinity, the surrogate-needle check,
and UTF-16 to byte conversions in both directions, although byte offsets
are UTF-16 indexes in an ASCII string. The fast path now covers every start
index: a positive start truncates and clamps to the length, where only the
empty needle is found, and the search runs from that byte offset.
An untyped write of a present declared field of a live record capsule now
stores into the native record through a per-shape `set` hook (checked like
the commit, refusing to the snapshot path for values the field cannot hold),
so the capsule keeps answering from the record instead of turning into a
permanent snapshot. Object.assign from a plain object takes the same path,
and checked conversions and matches read a capsule through a fresh,
uncached snapshot.
A checked conversion of a record capsule into a record type whose storage
is a same-offset prefix of the capsule's record (and whose extra fields hold
no references) passes the pointer: both names keep one object, writes show
through either, and the record remembers its own capsule parameters, so
every later box of it and the narrower type's reflective surfaces answer
for the whole object (decision D3). Other cross-layout conversions keep the
refreshed copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime vendored zlib 1.3.1 and archived it at -Os, and host C-lane
builds linked the system libz, so deflate, raw deflate and gzip output
differed from Node's at every level. The vendored copy is now Node
v24.22.0's deps/zlib (Chromium's zlib, 1.3.2.1-motley-285e94b), built at
-O3 with the units and defines Node's zlib.gyp selects per target: the
SSSE3 Adler-32 and SSE2 inflate and slide-hash code on x86-64, the NEON
and ARMv8 CRC-32 code on arm64, and the portable code on wasm32. The SIMD
units check the CPU at run time and give the same output, so compressed
bytes match Node's on every target.
zlibInputs (runtime-pack-common/scripts/zlib-inputs.mjs for the runtime
packs, vendor-inputs.ts for Zig cross builds and the sanitized runtime)
holds the per-target table, and a unit test keeps the two identical.
Every C-lane driver now links the vendored objects instead of -lz.
Chromium's BSD license ships beside zlib's in the runtime packs.
Corpus 8405 compares compressed bytes with Node across levels,
strategies, memLevel, windowBits, dictionaries and flushing streams;
8406 covers chunked inflation with small input pieces and output chunks.
A checked object (SCR_DYN_OBJ) now starts in layout mode: v.obj.layout names
its key sequence, a node of the thread's interned transition tree, and
v.obj.slots holds one 8-byte value per key, where every property used to take
a 48-byte entry. All properties of a layout-mode object are writable,
enumerable, configurable data properties. An accessor, a non-default
attribute, a delete, more than 64 keys, spent layout budgets, publication to
other threads, or any code that reads entries directly moves the object to
dictionary mode (today's entries) for good, so unported paths stay correct.
Layouts replace the dynamic tier's lazily computed hidden classes (ScrJsShape)
and keep their budgets. Inline caches key on the layout pointer and read the
slot directly; JSON.parse builds objects along layouts; stringify, key
enumeration, release, trace and the runtime read cache read slots in place.
The emitted index-signature checks read members through runtime accessors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each chunk a readable side delivered looked up its 'data' listeners by
name twice: once to decide whether a flowing stream emits directly, and
again in emit('data'), which scans the stream's event names even when a
piped stream has no 'data' listener at all. The stream now keeps a
data_listeners flag, set from the emitter registry's empty/nonempty
transitions through scr_emitter_observe, and reads it on those paths;
emit('data') is skipped when the stream has no 'data' listener.