page: input the compositor puts in a process's ring wakes its parked Workers

With every thread of a process parked on the global kernel's wake word,
nothing drained the wl input ring until a park's 20 ms bound ran out: every
click and key on mspaint could wait that long before the compositor pump
(on a scheduler turn) delivered it. The process's WlProxy now writes the
wake word's address into its arena block, the kernel memory travels with
the wl-memory message, and sendEvent bumps and notifies the word. The Worker
that wakes takes a scheduler turn, whose pump delivers the event and (since
b638c279) wakes the client's reader thread.

Leg: shims/browser/js/check-wl-input-wake.mjs, a Worker parked on the wake
word for 5 s returns 1.9-2.9 ms after the event; without the wake it sleeps
out its bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-12 06:15:39 -04:00
1 parent b638c27940
commit 1c658a5483
10 files changed
+140 -10

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
/**
* Does page input reach a process whose every thread is parked, well before
* the park's bound?
*
* A Wayland client's input goes page -> compositor -> the process's ring in
* the wl arena (`sendEvent`) -> the shim's compositor pump, which runs on a
* scheduler turn of one of the process's Workers. With every Worker parked on
* the global kernel's wake word (`gex_park`, 20 ms bound), nothing drained the
* ring until a bound ran out: up to 20 ms on every click and key, and the
* park-late diag found Wine's Wayland reader thread waiting out its bound.
* `sendEvent` now bumps the wake word the process's `WlProxy` names.
*
* This instantiates the kernel module in a second thread on shared memory and
* parks it on the wake word for 5 s (`k_park`, as `gex_park` does); the first
* thread claims a block as a guest would, then sends a pointer event 100 ms
* later. The park must end, woken (0), within a few milliseconds of the event.
*
* Usage: node check-wl-input-wake.mjs [gex_kernel.wasm]
*/
import { readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Worker, isMainThread, workerData, parentPort } from 'node:worker_threads';
import { makeWlChannel, WlProxy, sendEvent, setKernelMemory } from './wl-bridge.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..', '..');
async function instantiate(module, memory, process_) {
const inst = await WebAssembly.instantiate(module, {
env: { memory, process: process_, lazy_fetch: () => 0 },
});
return inst.exports;
}
if (isMainThread) {
const path = process.argv[2] || join(ROOT, 'web', 'public', 'gex', 'gex_kernel.wasm');
const module = await WebAssembly.compile(await readFile(path));
const memory = new WebAssembly.Memory({ initial: 2048, maximum: 65536, shared: true });
const process_ = new WebAssembly.Memory({ initial: 1, maximum: 65535, shared: true });
const k = await instantiate(module, memory, process_);
// The guest's half: a process claims its block (on its first publish).
const channel = makeWlChannel();
const proxy = new WlProxy(channel, () => {}, () => 7);
proxy.wakeAddr = () => k.k_wake_addr();
const guestMem = new WebAssembly.Memory({ initial: 1, maximum: 16, shared: true });
proxy.publish(guestMem, 0, 0);
if (!proxy.ok) {
console.error('wl input wake check FAILED: the proxy claimed no block');
process.exit(1);
}
// The compositor's half, in its own realm in the page: its own view of the
// arena, and the kernel memory a process's `wl-memory` message carried.
const compositorChannel = { arena: channel.arena };
setKernelMemory(compositorChannel, memory);
const seq = k.k_wake_seq();
const parked = new Promise((resolve, reject) => {
const w = new Worker(fileURLToPath(import.meta.url), {
workerData: { module, memory, process_, seq, ns: 5000 * 1e6 },
});
w.once('message', resolve);
w.once('error', reject);
});
await new Promise((r) => setTimeout(r, 100));
const sent = performance.timeOrigin + performance.now();
// Block 0: the first block a lone process claims.
sendEvent(compositorChannel, 0, 0 /* motion */, 10, 20, 1, 0, 1, 1);
const r = await parked;
const late = r.end - sent;
if (r.r !== 0 || late > 10) {
console.error(` the parked Worker returned ${r.r}, ${late.toFixed(1)} ms after the event (want 0, within 10 ms)`);
console.error(`wl input wake check FAILED: ${path}`);
process.exit(1);
}
console.log(` page input wakes a process whose threads are all parked (${late.toFixed(1)} ms after the event; the park's bound was 5 s)`);
process.exit(0);
} else {
const { module, memory, process_, seq, ns } = workerData;
const k = await instantiate(module, memory, process_);
const r = k.k_park(seq, BigInt(ns));
// `performance.now()` counts from each thread's own origin.
parentPort.postMessage({ r, end: performance.timeOrigin + performance.now() });
}
+8
View File
@@ -1013,6 +1013,14 @@ export class Gex {
get memory() { return this.#memory; }
/** The global kernel's memory and its wake word's address, or null: what
* a compositor bumps to wake this process's parked Workers. */
kernelWake() {
const k = this.#kernel;
if (!k?.k_wake_addr || !this.#kernelSpec?.memory) return null;
return { memory: this.#kernelSpec.memory, addr: k.k_wake_addr() };
}
/**
* Every guest block PC translated in this process so far, as a BigUint64Array
* (a fresh copy, safe to keep). The profile a sandbox run dumps to seed a
+29
View File
@@ -99,6 +99,9 @@ const B_HEARTBEAT = 1;
const B_COUNT = 2;
const B_GEN = 3;
const B_ACK = 4;
/** Where the global kernel's wake word is (`k_wake_addr`), or zero: what the
* compositor bumps after putting an event in this block's ring. */
const B_WAKE = 5;
const B_WORDS = 8;
const BLOCK_WORDS = B_WORDS + SLOTS * REC;
@@ -218,6 +221,11 @@ export function ackBlock(channel, block, gen) {
Atomics.store(a, blockBase(block) + B_ACK, gen);
}
/** The global kernel's memory, for `sendEvent`'s wake. One per page. */
export function setKernelMemory(channel, memory) {
if (memory && !channel.kernelMemory) channel.kernelMemory = memory;
}
/** Write one event into a process's ring, for its guest to pick up. */
export function sendEvent(channel, block, kind, a1 = 0, b1 = 0, c1 = 0, d1 = 0, client = 0, surface = 0) {
const a = new Int32Array(channel.arena);
@@ -233,6 +241,17 @@ export function sendEvent(channel, block, kind, a1 = 0, b1 = 0, c1 = 0, d1 = 0,
a[slot + 6] = surface | 0;
a[slot + 7] = 0;
Atomics.store(a, base + K_HEAD, head + 1); // publish last
// And a wake, as another guest's syscall would give: with every thread of
// the process parked, nothing drains the ring until a park's bound (20 ms)
// runs out. The Worker that wakes takes a scheduler turn, whose compositor
// pump delivers the event and wakes the client's reader in turn.
const wake = Atomics.load(a, blockBase(block) + B_WAKE);
const k = channel.kernelMemory;
if (wake && k && typeof SharedArrayBuffer === 'function' && k.buffer instanceof SharedArrayBuffer) {
const w = new Int32Array(k.buffer);
Atomics.add(w, wake >>> 2, 1);
Atomics.notify(w, wake >>> 2);
}
}
/**
@@ -265,6 +284,9 @@ export class WlProxy {
#tokenFn;
/** Called with the token once it is known, so the host can send the memory. */
onToken = null;
/** The global kernel's wake word address (`k_wake_addr`), or 0; asked when
* the block is claimed. See `B_WAKE`. */
wakeAddr = null;
/** Claim a block if this process does not have one yet. Returns whether it has. */
#ready() {
@@ -294,6 +316,7 @@ export class WlProxy {
this.#block = b;
this.#base = base;
this.#ring = ringBase(b);
this.#noteWake();
return;
}
for (let pass = 0; pass < 2; pass++) {
@@ -310,11 +333,17 @@ export class WlProxy {
Atomics.store(this.#a, base + B_GEN, 0);
Atomics.store(this.#a, base + B_ACK, 0);
Atomics.store(this.#a, base + B_HEARTBEAT, now);
this.#noteWake();
return;
}
}
}
#noteWake() {
const at = this.wakeAddr?.() | 0;
if (at) Atomics.store(this.#a, this.#base + B_WAKE, at);
}
get token() {
return this.#token;
}
+2 -1
View File
@@ -488,9 +488,10 @@ async function handle(msg) {
// The memory object, not its buffer: a shared memory that grows gives out a
// new buffer, and the compositor has to be able to see the new length.
gex.onWlToken = (token) => {
port.post({ kind: 'wl-memory', token, memory: gex.memory });
port.post({ kind: 'wl-memory', token, memory: gex.memory, kernel: gex.kernelWake()?.memory });
};
proxy.onToken = gex.onWlToken;
proxy.wakeAddr = () => gex.kernelWake()?.addr ?? 0;
} else if (msg.wlStub) {
// A headless compositor for node: the shim's gex-wayland runs and binds
// `wayland-0`, so a guest connects and inits exactly as in the browser, but
+4 -2
View File
@@ -18,6 +18,7 @@ import {
readScene,
ackBlock,
sendEvent,
setKernelMemory,
setOutputSize,
ROLE_TOPLEVEL,
ROLE_POPUP,
@@ -472,8 +473,9 @@ class Compositor {
* first window of a session appeared and the second never did, because its
* buffer was "past the end of memory".
*/
addMemory(token, memory) {
addMemory(token, memory, kernel) {
this.#memories.set(token, { memory, view: new Uint8Array(memory.buffer) });
setKernelMemory(this.#channel, kernel);
}
/** The current view of a process's memory, re-derived if it has grown. */
@@ -1498,7 +1500,7 @@ const onMessage = ({ data: m }) => {
startGl(m, canvas).catch((e) =>
post({ kind: 'gl-failed', error: e?.message || String(e) }));
} else if (m.kind === 'memory') {
comp?.addMemory(m.token, m.memory);
comp?.addMemory(m.token, m.memory, m.kernel);
} else if (m.kind === 'drop-memory') {
comp?.dropMemory(m.token);
// That process is gone: its GL context, its windows and their frames go
+2 -2
View File
@@ -283,8 +283,8 @@ export class Compositor {
}
/** A guest process's memory: where its surfaces' pixels are. */
addMemory(token, memory) {
this.#worker.postMessage({ kind: 'memory', token, memory });
addMemory(token, memory, kernel) {
this.#worker.postMessage({ kind: 'memory', token, memory, kernel });
}
resize(width, height) {
+3 -1
View File
@@ -47,6 +47,7 @@ import {
readScene,
ackBlock,
sendEvent,
setKernelMemory,
setOutputSize,
EV_MOTION,
EV_BUTTON,
@@ -390,8 +391,9 @@ export class DomDesktop {
* megabytes of a process that has grown to hundreds. Every surface past
* that point reads as zero -- a window that is simply blank, with nothing
* to say for itself. */
addMemory(token, memory) {
addMemory(token, memory, kernel) {
this.#memories.set(token, { memory, view: new Uint8Array(memory.buffer) });
setKernelMemory(this.#channel, kernel);
}
/** The current view of a process's memory, re-derived if it has grown. */
+4 -1
View File
@@ -199,10 +199,13 @@ export class HostX {
// -- the arena ---------------------------------------------------------
this.#proxy = new WlProxy(msg.wlChannel, () => this.emit({ kind: 'wl' }), () => this.#token);
// The server waits on the global kernel's wake word (`wait` below), which
// the compositor bumps when it puts input in this block's ring.
this.#proxy.wakeAddr = () => this.#k?.k_wake_addr?.() ?? 0;
// The compositor Worker reads an X window's pixels out of this memory, so
// it needs the memory itself -- the object, not a buffer, since a shared
// memory that grows hands out a new one.
this.emit({ kind: 'wl-memory', token: this.#token, memory: this.#x.wasmMemory });
this.emit({ kind: 'wl-memory', token: this.#token, memory: this.#x.wasmMemory, kernel: msg.kernel.memory });
// -- the transport the server will use ---------------------------------
this.#x.gexos = this.#gexos();
+2 -2
View File
@@ -759,7 +759,7 @@ async function runProgramHere(
xServer.onmessage = (e) => {
const m = e.data;
if (m.kind === 'log') term.writeln(m.text);
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory);
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory, m.kernel);
else if (m.kind === 'ready') listening(true);
else if (m.kind === 'failed') {
term.writeln(`gex: the X server failed: ${m.why}`);
@@ -989,7 +989,7 @@ async function runProgramHere(
if (m.kind === 'gl') glServe?.();
// A process that has a compositor block hands the compositor its memory:
// that is where the pixels of every one of its surfaces are.
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory);
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory, m.kernel);
else if (m.kind === 'wl') {
/* a publish happened; the compositor draws on its own clock */
}
+1 -1
View File
@@ -75,7 +75,7 @@ async function openCompositor(width, height) {
send,
// What `runProgram` calls on a compositor.
compositor: {
addMemory: (token, memory) => send({ kind: 'memory', token, memory }),
addMemory: (token, memory, kernel) => send({ kind: 'memory', token, memory, kernel }),
stop: () => {
clearInterval(timer);
send({ kind: 'stop' });