mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
browser: a harness's view of process memories is asked for, and held
`__gexMemories` held each threaded process's memory weakly, and a page leg reading a game's counter through it relied on something else on the page's main thread keeping that memory alive: the fork record the page now lets go of once the child's Worker has it. The memory was collected while the game ran, and the leg read nothing. The registry is now a test opt-in, `?memories=1` (or GEX_MEMORIES), read once at load: with it a process's memory is held until the process ends; without it nothing is registered or kept. tests/spawn-release: a fork child with a thread counts while the page reads its counter after collections (GEX_TEST_MEMORIES in run-node); the counter climbs, the memory is let go at the child's exit, and nothing is registered without the switch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_8b3490cccb4848b495dde1c5cdcacba0
This commit is contained in:
1 parent
505d65c845
commit
03a3b568c8
5 files changed
+100
-8
No files matched your search
@@ -38,6 +38,16 @@ function inform(text) {
|
||||
console.info(text);
|
||||
}
|
||||
|
||||
// `?memories=1` (or GEX_MEMORIES), read once at load: a test harness's view
|
||||
// of each threaded process's memory by pid, `globalThis.__gexMemories`, what
|
||||
// it reads a guest word from while the process's own Workers are busy in
|
||||
// wasm (a game's frame counter). Held until the process ends; off, nothing
|
||||
// is registered and nothing is kept.
|
||||
const MEMORIES = !!(globalThis.GEX_MEMORIES || globalThis.process?.env?.GEX_MEMORIES || (typeof location !== 'undefined' && new URLSearchParams(location.search).get('memories') === '1'));
|
||||
function forgetMemory(pid) {
|
||||
if (MEMORIES) globalThis.__gexMemories?.delete(Number(String(pid).split(':')[0]));
|
||||
}
|
||||
|
||||
// A library or image loaded for the first time this session, said once
|
||||
// rather than once per process (each Wine process maps the same dozen DLLs).
|
||||
// Every Worker says what it installs on the 'gex-fetch' channel
|
||||
@@ -1079,13 +1089,8 @@ export class ProcessHost {
|
||||
// Worker. Nothing waits for it.
|
||||
this.#threads++;
|
||||
const key = `${pid}:${m.thread.tid}`;
|
||||
// Each process's memory by pid (a thread's Worker is keyed `pid:tid`), weakly (an exited process's memory
|
||||
// is not kept): what a harness reads a guest word from while the
|
||||
// process's own Workers are busy in wasm (e.g. a game's
|
||||
// frame counter). A threaded process registers here.
|
||||
if (typeof WeakRef === 'function' && m.thread.memory) {
|
||||
(globalThis.__gexMemories ??= new Map()).set(Number(String(pid).split(':')[0]), new WeakRef(m.thread.memory));
|
||||
}
|
||||
// A threaded process's memory, for a harness (`MEMORIES`).
|
||||
if (MEMORIES && m.thread.memory) (globalThis.__gexMemories ??= new Map()).set(Number(String(pid).split(':')[0]), m.thread.memory);
|
||||
if (globalThis.GEX_ATTACH_TRACE) console.error(`[spawn] host got 'thread' for ${key}, taking a Worker (t=${Date.now()})`);
|
||||
// From the warm pool, as a process's: a fresh Worker was ~100 ms of
|
||||
// script load and shim instantiation before the thread could start,
|
||||
@@ -1198,6 +1203,7 @@ export class ProcessHost {
|
||||
const proc = Number(String(pid).split(':')[0]);
|
||||
const c = this.#carried.get(proc);
|
||||
this.#onExit(m, proc);
|
||||
forgetMemory(proc);
|
||||
this.#release(pid, w);
|
||||
if (c?.resolve) {
|
||||
this.#carried.delete(proc);
|
||||
@@ -1324,10 +1330,12 @@ export class ProcessHost {
|
||||
}
|
||||
// Before the release: releasing terminates the Worker.
|
||||
this.#onExit(m, pid);
|
||||
forgetMemory(pid);
|
||||
this.#release(pid, w, !!m.giveBack);
|
||||
resolve(m.status);
|
||||
break;
|
||||
case 'threw':
|
||||
if (!String(pid).includes(':')) forgetMemory(pid);
|
||||
this.#release(pid, w);
|
||||
// Here as well as in the Worker: only this side knows which process
|
||||
// fell over, and a child's promise often has nobody awaiting it.
|
||||
|
||||
@@ -76,6 +76,23 @@ const SHIM_INFIX = ARCH_INFIX + (MEM64 ? '.m64' : '');
|
||||
// thread spawn/adopt timing is visible from the host side, not just the Worker.
|
||||
if (process.env.GEX_ATTACH_TRACE) globalThis.GEX_ATTACH_TRACE = 1;
|
||||
if (process.env.GEX_DENSE_DEBUG) globalThis.GEX_DENSE_DEBUG = 1;
|
||||
// GEX_TEST_MEMORIES=ADDR (tests/spawn-release; with GEX_MEMORIES=1 and
|
||||
// node --expose-gc): twice a second, after a collection, the 32-bit word at
|
||||
// ADDR in each memory `__gexMemories` holds, as a page leg reads a game's
|
||||
// frame counter; and at exit how many it still holds.
|
||||
if (process.env.GEX_TEST_MEMORIES) {
|
||||
const at = Number(process.env.GEX_TEST_MEMORIES);
|
||||
setInterval(() => {
|
||||
globalThis.gc?.();
|
||||
const all = globalThis.__gexMemories;
|
||||
if (!all?.size) console.error('[test] memories: none');
|
||||
for (const [pid, mem] of all ?? []) {
|
||||
const v = mem.buffer.byteLength >= at + 4 ? new DataView(mem.buffer).getUint32(at, true) : -1;
|
||||
console.error(`[test] memories: pid ${pid} word ${v}`);
|
||||
}
|
||||
}, 500).unref();
|
||||
process.on('exit', () => console.error(`[test] memories at exit: ${globalThis.__gexMemories?.size ?? 0}`));
|
||||
}
|
||||
|
||||
// Options come first and are named, so a guest argument that happens to
|
||||
// contain an `=` -- `lua -e 'local x=1'`, say -- reaches the guest intact.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// What a game's page leg does: a harness reads a counter from the memory of
|
||||
// a process that is a fork child (its memory came in its fork record) with
|
||||
// a thread of its own (which registers that memory with the page), while
|
||||
// the process runs. The record is let go once the child's Worker has it;
|
||||
// the harness's view of the memory must last as long as the process.
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
volatile unsigned counter;
|
||||
|
||||
static void *tick(void *arg) {
|
||||
(void)arg;
|
||||
struct timespec t = {0, 20 * 1000 * 1000};
|
||||
for (int i = 0; i < 200; i++) {
|
||||
counter++;
|
||||
nanosleep(&t, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
pthread_t th;
|
||||
pthread_create(&th, 0, tick, 0);
|
||||
pthread_join(th, 0);
|
||||
printf("child counted %u\n", counter);
|
||||
fflush(stdout);
|
||||
_exit(0);
|
||||
}
|
||||
int st;
|
||||
waitpid(pid, &st, 0);
|
||||
printf("child %s\n", WIFEXITED(st) && WEXITSTATUS(st) == 0 ? "ok" : "failed");
|
||||
return 0;
|
||||
}
|
||||
@@ -33,3 +33,32 @@ else
|
||||
echo "FAIL spawn-release browser: $out; records released: $released, kept: $kept"; grep -a '\[test\]' "$w/browser.err" | tail -3
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A page leg's view of a game's memory (peek.c): `?memories=1` (here
|
||||
# GEX_MEMORIES=1) registers a threaded process's memory for the harness, and
|
||||
# with the fork record let go that registry is all that must keep it. Read
|
||||
# after a collection twice a second (GEX_TEST_MEMORIES), the child's counter
|
||||
# must be there and climbing while it runs, and let go once it has exited;
|
||||
# without the switch nothing is registered.
|
||||
"$cc" -O1 -static -no-pie -o "$w/peek" "$here/peek.c" -lpthread || exit 1
|
||||
at=0x$("${cc%gcc}nm" "$w/peek" | awk '/ counter$/ {print $1}')
|
||||
peek() { # LOG [env...]
|
||||
log=$1; shift
|
||||
(cd "$w" && env "$@" GEX_TEST_MEMORIES="$at" GEX_SHARED=1 timeout 120 node --expose-gc "$root/shims/browser/js/run-node.mjs" \
|
||||
--file "$w/peek=$w/peek" "$seed" "$w/peek" 2> "$log" | grep -av '^\[gex\]')
|
||||
}
|
||||
out=$(peek "$w/peek.err" GEX_MEMORIES=1)
|
||||
words=$(grep -a '\[test\] memories: pid' "$w/peek.err" | sed 's/.* word //')
|
||||
climbs=$(awk 'NR > 1 && $1 > p {n++} {p = $1} END {print n + 0}' <<< "$words")
|
||||
left=$(grep -a '\[test\] memories at exit' "$w/peek.err" | sed 's/.*: //')
|
||||
if [ "$out" = "child counted 200
|
||||
child ok" ] && [ "$climbs" -ge 3 ] && [ "$left" = 0 ]; then
|
||||
echo "ok spawn-release memories (the counter read $climbs times higher while the child ran, then let go)"
|
||||
else
|
||||
echo "FAIL spawn-release memories: $out; readings climbed $climbs times; held at exit: ${left:-?}"; exit 1
|
||||
fi
|
||||
out=$(peek "$w/peek-off.err")
|
||||
if grep -aq '\[test\] memories: pid' "$w/peek-off.err"; then
|
||||
echo "FAIL spawn-release memories: registered without the switch"; exit 1
|
||||
fi
|
||||
echo "ok spawn-release memories off (nothing registered)"
|
||||
+1
-1
@@ -34,7 +34,7 @@ async function main() {
|
||||
? [Math.max(640, document.documentElement.clientWidth), Math.max(480, document.documentElement.clientHeight)]
|
||||
: (q.get('size') || '1024x768').split('x').map(Number);
|
||||
const switches = Object.fromEntries(q);
|
||||
for (const k of ['tar', 'exe', 'args', 'argv', 'env', 'cwd', 'size', 'persist', 'persistdb', 'lazy', 'preload', 'pack', 'zstd', 'mtimes', 'gl', 'glbackend', 'hostx', 'provsrc', 'stateful']) delete switches[k];
|
||||
for (const k of ['tar', 'exe', 'args', 'argv', 'env', 'cwd', 'size', 'persist', 'persistdb', 'lazy', 'preload', 'pack', 'zstd', 'mtimes', 'gl', 'glbackend', 'hostx', 'provsrc', 'stateful', 'memories']) delete switches[k];
|
||||
// `provsrc=url,...`: provider sources from outside gex (GEX_PROVIDER_DIRS
|
||||
// natively), each named by its file's stem.
|
||||
const providerSources = q.get('provsrc')
|
||||
|
||||
Reference in new issue
Block a user