A Response body's stream gets a web ReadableStream view when a member the
fetch stream lacks is read (for example `body.tee`). The view's source holds
the stream, and the stream released the view only when that source was its
last holder. While the transfer runs it holds the stream too, so a dropped
body with a view kept a reference count of 2: the unobserved-response
discard never ran, the paused socket was never resumed, and a compressed
response dropped before HTTP EOF kept the process alive.
The stream now releases the view when only the view's source and the live
transfer hold it, and then takes the unobserved-response discard, as a
body without a view does.
The static-abandon-view fixture reads `body.tee` and `body.locked` of a
4 MiB incompressible gzip response from the new /gzip-incompressible route
and drops it. It hung until the test's timeout before this change and now
exits as Node does.
process.memoryUsage() reported external and arrayBuffers as 0. arrayBuffers
is now the storage of the ArrayBuffers, typed arrays, Buffers, DataViews
and SharedArrayBuffers the program holds, and external the same storage
without SharedArrayBuffers, as Node splits them. A view over existing
storage adds nothing. v8.getHeapStatistics().external_memory reads the
same figure as external.
Each ScrBytes owner records the bytes it counts in the struct's padding,
so ScrBytes stays 64 bytes and the offsets compiled code reads hold; a
static assertion pins them. Owners are counted in a per-instance counter,
because their reference counts are not atomic and they are released on
their instance's thread. Shared storage moves its owner's count to a
process-wide atomic counter, since any thread may release it.
The previous change avoided offsetting a null pointer with a branch in
scr_arr_alloc_start. The function is inlined into scr_arr_release and
scr_arr_destroy, so the branch changed their control flow, and the
functions that changed since the committed runtime profile was collected
then held 5.2% of its counts, over runtime-profile.test.ts's 5% limit.
The address is now computed as an integer, which is defined for a null
block and leaves the callers' control flow as it was.
scr_arr_alloc_start subtracted the prefix from the dense block even when
the array had no storage, which computes NULL - 0. That is undefined in C,
and the macOS lane's UndefinedBehaviorSanitizer reports it
("applying zero offset to null pointer" at scr_array.c:60) in
scr_json.test.ts's weak-map program. Without a prefix, the function now
returns the block as it is.
In a TypeScript source, a builtin global used as an object literal member
(`const o: any = { j: JSON }`) failed to lower, and the member silently
became a closure that threw when called. Node reads the global itself, so
`typeof o.j` answered "function" instead of "object", two reads were never
equal, and `o.j.stringify(x)` threw "is not a function". There was no
diagnostic.
A builtin read into a slot the checker types as any or unknown now holds
the value a JavaScript source reads: the JSON, Math and Reflect namespace
objects, Buffer, and functions such as structuredClone, atob and
queueMicrotask. This covers object and array
literal members, arguments, initializers and return values. A member of a
literal whose own slot is any takes the literal's slot. Array, setTimeout
and setImmediate keep their refusal: their values lack Array's statics and
the timers' util.promisify.custom getter.
Object literal members of TypeScript sources no longer defer a failed
lowering to a runtime closure. A builtin with no value (Intl, Proxy,
parseInt, process, a typed slot) reports SC2020 at compile time.
util.inspect now prints a plain object's Symbol.toStringTag as Node does:
`Object [Math] {}`, `[Object [Y]]` past the depth limit, and
`[Object: null prototype] [N] {}`. The tag is read through the prototype
chain and getters, and it is skipped when it is empty, not a string, the
name "Object", listed as an own enumerable key, or its getter throws.
A shared view written to stdout or stderr went out in 64 KiB pieces through
scr_stdio_write, so a replaced process.stdout.write received the first
piece alone, and a false return, which only reports backpressure, ended the
loop: a view larger than 64 KiB lost the rest. With a write hook, the view
is now copied whole and passed in one call, as Node passes it; the bounded
pieces go to the stream itself through scr_stdio_write_raw, which never
stops early.
tests/corpus/shared-bytes-stdio-write.ts replaces process.stdout.write with
a function that records each chunk's length and returns false, and writes a
150,000-byte shared view through it.
Adding scr_stats_of_path and scr_stats_zero to scr_lib.c moved every
program's executable: scr_lib.o is linked everywhere, so its new symbols
and strings shifted addresses even for programs that never watch a file.
The two entry points now live in an SCR_FS_WATCH_FILE build of scr_lib.c,
the matrix's variant for programs whose feature set has watchFile, and the
poll unit calls the renamed scr_stats_poll_path. Programs that do not poll
link the default object unchanged: compiled without the define, scr_lib.o
is byte-identical to the base (verified by comparing the two objects).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linux 5.9 and 5.10 answer 0 instead of EAGAIN for an RWF_NOWAIT read of
data that is not cached. The inline read took that 0 as end of file and
ended the ReadStream early. A zero from the inline read now goes to the
pool, whose read returns the data or confirms end of file; this costs one
pool round trip at the end of each file.
fs-stream-nowait.test.ts preloads a preadv2 shim that answers 0 for every
RWF_NOWAIT read and checks that a ReadStream still reads the whole file,
as Node does.
On Linux, an fs stream's read or write of a regular file first tries
preadv2/pwritev2 with RWF_NOWAIT on the loop thread. When the page cache
holds the data, the step completes there and its completion waits for the
next loop turn (scr_work_submit_done), as a pool job's completion does,
so timers and I/O still run between steps. EAGAIN, a kernel or file
system without RWF_NOWAIT, and other files take the pool as before.
- A read the cache holds only part of finishes on the pool, so each chunk
is the one a single read() returns.
- A partial write completes with the bytes written; the stream submits
the rest, as after any partial write.
- Writes stay on the pool when RLIMIT_FSIZE is finite, so SIGXFSZ cannot
reach the loop thread.
scr_work_submit_done queues a completed job on a list the loop moves to
the done queue at its next sleep step, which is skipped while jobs wait.
Corpus 8530 covers a positioned read with a small highWaterMark, a write
stream with a start position into an existing file, and an append stream
over cached files, against Node.
fs.read's options forms read the position option a second time when it
was not a bigint, so a getter ran twice and a stateful one could move the
read. The option is now read once.
A bigint position was checked against the length before `length |= 0`.
A fractional or infinite length made the range check build its bound
from a null BigInt and crash. The check and the read now take the
converted length, so such a read completes with zero or the truncated
count of bytes, as in Node.
The exit-time cleanup of the path module objects swapped each member
value out of the object's storage by reading v.obj.slots and
v.obj.entries directly. It now goes through scr_dyn_obj_count and a new
scr_dyn.h accessor, scr_dyn_obj_exchange_value_at, which stores a value at
an index without reading the keys, so the cleanup still runs after the
exit-time layout reset.
A path family read by untyped code is a plain object holding Node's
members in Node's order, built once per family and kept in the
builtin-module registry. Object.keys, Object.entries, Object.assign,
spreads, for-in and own-property checks over it see those members.
path.posix.posix and the cross-family members refer to the registered
objects; the registry releases their members at exit through the objects'
own storage.
Every builtin function value that becomes untyped boxes as its checked
twin, under the builtin's name and length, after lowering: a module
object's member, an assertion's argument, and a read of a binding whose
only stores are that builtin (a const local, or a module-level binding).
`ns.join === path.posix.join` holds for an untyped path namespace, and
`const join = path.join` passed to an untyped function compares equal to
path.join and validates its arguments with Node's errors.
The lowerer journals the checked-twin table (each builtin's data, display
name and location), so a unit replayed from the journal keeps its twins.
process.emitWarning checks its arguments in Node's order: an options
object replaces type with `options.type || "Warning"` and code with
`options.code`, type and code are checked before the warning and for
Error warnings too, an empty type names the warning "Warning", and a
DeprecationWarning type returns at once under process.noDeprecation.
Wrong kinds throw ERR_INVALID_ARG_TYPE with Node's "Received ..." tail.
An instance of an Error subclass is an Error warning, and a typed record
in the options position is read as the options object. The default
report reads the warning's message, code and detail as property reads
do, so an Error subclass's own code is reported. A deprecation thrown
under process.throwDeprecation is thrown as the Error it encodes, so the
uncaught report names it.
process.kill throws ERR_UNKNOWN_SIGNAL for an unknown signal name, as
subprocess.kill does, and a failed kill(2) is Node's errnoException with
errno, code and syscall. A failed process.chdir names the working
directory as path and the target as dest.
os.userInfo() reads the passwd entry of the effective uid: uid and gid
are the entry's pw_uid and pw_gid (-1 on Windows), not the process ids.
os.homedir() answers $HOME whenever it is set, an empty value included.
The minimal test/common for Node's test/parallel files gains isAIX,
isIBMi and isSunOS.
A byte write to a replaced process.stdout.write or process.stderr.write
was boxed with the 'data' chunk helper, which answers a string while a
listener of a socket or HTTP response with setEncoding("utf8") runs. A
`process.stdout.write(buffer)` from such a listener reached the
replacement as a string. Node passes the Buffer it was given.
The stdio hook now boxes a byte write as a Buffer whatever listener is
running.
A worker thread's large blocks come from glibc heaps, and the first large
block of each heap made the allocator check /proc/self/maps before
advising the heap huge pages. The check was skipped only for the heap the
thread advised last, so threads that switched between heaps read the maps
again at every switch: 9,000 to 11,000 reads in a type check of T3 web with
four checkers. A process-wide table now records every heap decided, and
each heap is read once.
The stream completion closures carried SCR_CLOSURE_KIND_RECEIVER_FREE, so
compiled callers skipped the receiver bracket around cb(). A completion
runs user code synchronously (the write(chunk, callback) callback, the
next queued _write), and that code reads the ambient receiver, so the
flag is removed and the completions bind `this` as any unbound call
does. Corpus programs 8531 and 8532 call a saved completion from inside
another object's method, from JavaScript and from a TypeScript class
method that JavaScript calls, and check `this` in the callbacks it runs
against Node.
- watcher.off/removeListener of a listener that is not the last one moves
the later listeners down instead of up, so the removed closure leaves
the list.
- The watchFile adapters for zero- and one-parameter listeners release
the Stats their listener has no parameter for, and the fs.watch adapter
releases the event string once a dynamic value holds it.
- A one-parameter fs.watch listener whose eventType is dynamic receives a
boxed string through the emitted adapter.
- fs.watch, fs.watchFile and fs.unwatchFile throw ERR_INVALID_ARG_VALUE
for a path holding a null byte, and a new watchFile poller throws
ERR_OUT_OF_RANGE when interval is not a uint32, as Node does. Both
watchFile libCalls check for the pending exception.
- fs.watchFile and fs.unwatchFile have surface-manifest entries, so a
fence can deny them.
- The self-hosted feature records list watchFile, and the node-types
fence fixture uses fs.openAsBlob in place of the now-lowered
fs.watchFile.
Tests: tests/corpus/fs-watch-listener-shapes.ts (listener shapes, off of
a middle listener, null-byte path) with its order baseline and cost file,
the validation cases in fs-watch-file-polling.ts, and
tests/harness/fs-watch-leaks.test.ts, which runs both programs under
LeakSanitizer in the sanitized lane.
- A typed write or transform callback whose encoding parameter is a
pointer union with an undefined arm receives the null pointer directly
when an object-mode write passed no encoding, instead of converting the
encoding value through a dyn box
(scr_stream_chunk_encoding_undefined).
- The stream completion callbacks carry SCR_CLOSURE_KIND_RECEIVER_FREE:
their thunks never read `this`, so compiled callers skip the receiver
bracket.
- The completion thunks borrow the stream from the closure's box instead
of retaining and releasing it on every call; the closure, alive for the
call, holds the box.
- settle_next, entry_convert and clear_buffer_each test their early-out
conditions in inline wrappers before calling the full functions.
A corpus program covers the encoding argument in object mode, byte mode
and transforms.
scr_fs_write_once cached whether the calling thread blocks SIGPIPE and
SIGXFSZ in an SCR_TL variable. SCR_TL is empty in builds without thread
instances, so a pool thread's answer applied to the main thread too, and
a later writeSync to a closed pipe ended the process with SIGPIPE
instead of throwing EPIPE. The cache is _Thread_local now. The
fs-write-sync fixture covers a write stream followed by writeSync on a
closed stdout.
A Console instance over process stdio passed a patched write the line and
an error handler, and discarded what the write threw, whatever its
ignoreErrors setting. Node's kWriteToConsole does that only with
ignoreErrors (the default, and the global console). With ignoreErrors
false, from the options or the third constructor argument, it calls
stream.write(line) alone and lets a throw propagate out of the console
method.
The console write path now takes the instance's ignoreErrors, so such an
instance passes only the line and keeps the write's exception pending.
Instances bound to other streams already followed the setting.
- A worker writes the completion wake byte only when the pipe has none.
The loop reads it, with the queue lock held, when it takes the last
waiting completion. The pipe holds a byte exactly while completions
wait, so a burst of completions costs one write and one read instead of
one write per completion and a read loop per dispatch.
- The fs pool's threads keep SIGPIPE and SIGXFSZ blocked for their
lifetime. scr_fs_write_once checks once per thread whether both are
blocked; on such a thread a write makes no signal-mask calls, and a
write that fails with EPIPE or EFBIG consumes the signal it raised.
Other threads keep the per-write guard.
fs-write-sync.test.ts covers EPIPE and EFBIG from write streams, whose
writes run on the pool's threads, and matches Node's errors.
Three runtime changes that lower peak memory without changing behavior:
- In programs with workers, process.stdout.write and process.stderr.write
of a view into a SharedArrayBuffer read it through a 64 KiB buffer
instead of copying the whole view first, so writing a 32 MB shared view
no longer holds a second 32 MB. Programs without workers keep the direct
write.
- The one-slot string spare keeps freed blocks of 512 bytes to 1 MiB only.
A larger block stayed resident until a string of about its size came
along, which a program that replaces its input with an edited copy never
does.
- The main thread trims its arena when its event loop waits with nothing
ready, once it has carved 16 MB more since its last trim, as a parking
thread does: pages that hold only free blocks go back to the system while
the program waits on its workers.
A union retype between two shape unions (or two class unions) shares the
pointer when both encode their unit arms the same way. Narrowing a field
read past `=== undefined` or `=== null` retypes a union with a unit arm
to one without it, so the encodings differ and each listed arm was
rebuilt: a retain, a tag switch, a second retain and a release, for the
same pointer. The unit encodings only matter when the value may hold a
unit arm. When the retype lists none of the source's unit tags, every
value is a record or class pointer whose arm the target derives from the
record or class itself, so the pointer is the target value.
The rebuild's release also made the pointer a nursery cycle candidate,
and in the native compiler those candidates kept the nursery bypass's
probes of closures and boxes succeeding. Without them, a probe window can
open while young cycles of the type are held by older garbage that the
bypass promoted earlier. The window then frees nothing, the type stays
bypassed, and the garbage waits for a mature pass, which on a cold tsc-ts
build raised the compiler's peak RSS by about 100 MB. A nursery probe
window that opens now makes the next release-triggered pass that would
run at the nursery run at the mature level, which reclaims that garbage
before the window is judged.
Node's console writes each formatted line with stream.write(line,
errorHandler), so a replaced process.stdout.write sees a string and a
function; a Buffer write arrives as a Buffer. The stdio write hook carried
only a `wide` bit, so every intercepted chunk became a JavaScript string and
console lines carried a single argument.
Replace that bit with a `flags` int (SCR_STDIO_HOOK_WIDE, _CONSOLE, _BYTES)
and keep the hook signature at four parameters, so the console write path and
the byte paths differ only in what they pass. scr_stdio_intercept now builds a
chunk instead of a string, boxes SCR_STDIO_HOOK_BYTES as a Buffer, and appends
a console error handler for SCR_STDIO_HOOK_CONSOLE. Text callers of the old
string entry point move to a new scr_stdio_write_text.
Corpus: stdout-patch-console-arguments.js.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>