Revert "browser: the page lets go of a fork child's record once its Worker has it"

This reverts commit 505d65c8: with it the Noita page never reaches its world.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_8b3490cccb4848b495dde1c5cdcacba0
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-24 12:29:53 -04:00
1 parent 607dc757ff
commit c34567c8f2
3 files changed
+5 -101

No files matched your search

+5 -36
View File
@@ -129,10 +129,6 @@ const GIVEN_BACK_MAX = 2;
* many bytes they may hold together: memory the page held anyway while
* those processes ran, not more. */
const RECYCLE_COUNT = 4;
/** A test switch (tests/spawn-release): whether a running fork child's
* record, its parent's modules with it, can be collected once the child's
* Worker has it. Node only, with `--expose-gc`. */
const TEST_FORK_RECORDS = !!globalThis.process?.env?.GEX_TEST_FORK_RECORDS;
const RECYCLE_BYTES = 768 << 20;
/** How far a fork record's image reaches: what its child's memory grows to. */
@@ -358,19 +354,6 @@ export class ProcessHost {
/// first touch was most of the cost of copying a fork's image in. Kept
/// newest last, up to RECYCLE_COUNT and RECYCLE_BYTES.
#recycled = [];
/** TEST_FORK_RECORDS: each fork child's record, weakly, by pid. */
#forkRecords = new Map();
/** TEST_FORK_RECORDS: said a moment after the child's Worker has its
* record. A scope of its own, so the timer holds nothing of the caller's. */
#checkForkRecord(pid) {
const record = this.#forkRecords.get(pid);
if (!record) return;
this.#forkRecords.delete(pid);
setTimeout(() => {
globalThis.gc?.();
console.error(`[test] pid ${pid}: fork record ${record.deref() ? 'kept' : 'released'} while it runs`);
}, 500);
}
#warmMade = 0;
#warmUsed = 0;
#stackOf = new Map();
@@ -540,14 +523,6 @@ export class ProcessHost {
w.on?.('error', fail);
w[LISTENING] = { onEvent, onMsg, fail };
this.#pending.set(pid, { ...msg, pid });
const kind = msg.kind;
// The listeners above live as long as the process and share this
// scope: holding `msg` here kept a fork's whole record (every module
// its parent had, with their metadata) on this thread for the child's
// life, and a child's record names its parent's modules too -- some
// twenty thousand modules on a desktop's page. `#pending` has it until
// the Worker is ready.
msg = null;
// `#pending` pins the whole fork record (image bytes included) until the
// child reports ready. A child that never starts leaves ~19 MB pinned.
// See task 145.
@@ -563,7 +538,7 @@ export class ProcessHost {
// One line per process, to `debug`: a process that never starts leaves
// no other trace -- the fork is logged by the parent, the exit by the
// child. #137.
narrate(`[host] pid ${pid}: worker taken, ${kind} held for ready`);
narrate(`[host] pid ${pid}: worker taken, ${msg.kind} held for ready`);
// And now whatever it said while it was still in the pool.
const held = w[EARLY];
if (held) {
@@ -912,7 +887,6 @@ export class ProcessHost {
// Timed separately: the fork-to-adopt gap is 549ms and unaccounted --
// this `postMessage` (the 15.4 MiB image copy) or the closure below.
const tSent = performance.now();
if (TEST_FORK_RECORDS) this.#checkForkRecord(pid);
this.#streamClosure(w, closure);
if (this.#extra?.debugExit) {
this.#onOutput(
@@ -1063,18 +1037,13 @@ export class ProcessHost {
else this.#flavourOf.delete(m.child.pid);
}
this.#onFork(m.child, pid);
if (TEST_FORK_RECORDS) this.#forkRecords.set(m.child.pid, new WeakRef(m.child));
// Started and let run -- nobody waits for a child. The handler below
// outlives this call by the child's whole life, so it holds the pid
// alone, and the message is let go (see `#spawn`).
const child = m.child.pid;
this.#spawn(child, { kind: 'adopt', child: m.child }).catch((e) => {
this.#onOutput(2, `gex: process ${child} failed: ${e.message}\n`, pid);
// Started and let run -- nobody waits for a child.
this.#spawn(m.child.pid, { kind: 'adopt', child: m.child }).catch((e) => {
this.#onOutput(2, `gex: process ${m.child.pid} failed: ${e.message}\n`, pid);
// As an event too: this is caught, so no `error` fires and a harness
// may miss the line above. See task 146.
crashed(`pid ${child}: ${e.message}`);
crashed(`pid ${m.child.pid}: ${e.message}`);
});
m = null;
break;
}
case 'execAway': {
-30
View File
@@ -1,30 +0,0 @@
// A fork child that runs on for a while after its parent has translated
// code: while it runs, the page's main thread must not keep its fork
// record, which names every module the parent had.
#include <stdio.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
__attribute__((noinline)) long work(long n) {
long s = 0;
for (long i = 0; i < n; i++)
s += i % 5;
return s;
}
int main(void) {
long s = work(200000);
pid_t pid = fork();
if (pid == 0) {
struct timespec t = {2, 0};
nanosleep(&t, 0);
printf("child %ld\n", work(1000));
fflush(stdout);
_exit(0);
}
int st;
waitpid(pid, &st, 0);
printf("parent %ld, child %s\n", s, WIFEXITED(st) && WEXITSTATUS(st) == 0 ? "ok" : "failed");
return 0;
}
-35
View File
@@ -1,35 +0,0 @@
#!/bin/bash
# The page's main thread lets go of a fork child's record once the child's
# Worker has it, though the child runs on: the record names every module the
# parent had (its own and its parents'), and held for each child's life they
# came to some twenty thousand modules on a desktop's page.
# (GEX_TEST_FORK_RECORDS: the host says, half a second after the hand-over
# and a collection, whether the record is still reachable.)
#
# tests/spawn-release/run.sh
#
# Needs ~/toolchains/aarch64-linux-musl-cross (GEX_TOOLCHAIN) and the
# shared build in web/public/gex (GEX_SHARED=1 shims/browser/build.sh);
# skipped without them.
set -u
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
tc=${GEX_TOOLCHAIN:-$HOME/toolchains/aarch64-linux-musl-cross}
cc=$tc/bin/aarch64-linux-musl-gcc
seed=$root/web/public/gex/seed.shared.wasm
for t in "$cc" "$seed"; do [ -e "$t" ] || { echo "skip: $t is not there"; exit 0; }; done
w=${WORK:-$root/target/spawn-release-test}
rm -rf "$w"; mkdir -p "$w"
"$cc" -O1 -static -o "$w/fork" "$here/fork.c" || exit 1
want="child 2000
parent 400000, child ok"
out=$(cd "$w" && GEX_TEST_FORK_RECORDS=1 GEX_SHARED=1 timeout 120 node --expose-gc "$root/shims/browser/js/run-node.mjs" \
--file "$w/fork=$w/fork" "$seed" "$w/fork" 2> "$w/browser.err" | grep -av '^\[gex\]')
released=$(grep -ac 'fork record released while it runs' "$w/browser.err")
kept=$(grep -ac 'fork record kept while it runs' "$w/browser.err")
if [ "$out" = "$want" ] && [ "$released" -ge 1 ] && [ "$kept" -eq 0 ]; then
echo "ok spawn-release browser"
else
echo "FAIL spawn-release browser: $out; records released: $released, kept: $kept"; grep -a '\[test\]' "$w/browser.err" | tail -3
exit 1
fi