mailbox S3: N5 flood spec + work log; bump wxwidgets (plain-call pumps)
N5 unit gates (scheduler-shim.test.ts): 500-call mutator flood strict FIFO, time-boxed chunking proven under load, wake-drain FIFO. Gates for S3: asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45, kicad 6/6 (incl. modal-stack + contextmenu-scrollbar) on a fresh C-lane build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
parent
603e260885
commit
5f29cd7be9
3 changed files with 174 additions and 1 deletions
|
|
@ -211,6 +211,22 @@ rollback = the `WX_SCHEDULER=0` build + a per-step tag.
|
||||||
(#13302) is removed. The root context never awaits JS (13 §6b). **Gate:** net green with
|
(#13302) is removed. The root context never awaits JS (13 §6b). **Gate:** net green with
|
||||||
emphasis on `contextmenu*`, `menu`, `popup`, coroutine apps (the June regression pair,
|
emphasis on `contextmenu*`, `menu`, `popup`, coroutine apps (the June regression pair,
|
||||||
13 §6d, must both stay green simultaneously).
|
13 §6d, must both stay green simultaneously).
|
||||||
|
> **Work log 2026-08-05 — S3 LANDED and gated.** All three remaining `await ccall(
|
||||||
|
> 'ProcessEvents',{async:true})` pumps — modal (`dialog.cpp` startModal), nested loop
|
||||||
|
> (`evtloop.cpp` wxWasmRunNestedLoop), popup (`wx-dom.js`) — now drive ProcessEvents as a
|
||||||
|
> PLAIN export call on scheduler builds (runtime-gated on the shim marker; legacy paths
|
||||||
|
> byte-identical). With the v0.1.28 top-level tick, NO pump awaits a suspending export
|
||||||
|
> anymore: the #13302 boundary is gone from the scheduler variant entirely. Overlapping
|
||||||
|
> pump ticks are parked-safe by ProcessEvents' existing gate; a chain that dies mid-park
|
||||||
|
> surfaces via the window error taps instead of the pump's await-catch (containment
|
||||||
|
> unchanged: fatal-screen + `wx_dispatch_abandon`). **Gates:** asyncify 9/9, coroutine
|
||||||
|
> 39/39 (the June pair green simultaneously), wx-chromium modal-heavy 45/45 (+menu,
|
||||||
|
> wizard, filedialog), kicad 6/6 incl. import-settings-modal-stack + contextmenu-scrollbar
|
||||||
|
> on a fresh warm-cache C-lane build. **N5 landed** alongside (scheduler-shim.test.ts:
|
||||||
|
> 500-call flood strict-FIFO, time-box chunking proven, wake-drain FIFO — 3/3).
|
||||||
|
> **Left open:** the doc-12 "root fiber" formalization (main loop as ctx object,
|
||||||
|
> `set_main_loop`-style top-off-asyncify) — not needed for the #13302 goal; revisit at S4
|
||||||
|
> if the waits migration wants real park/resume methods.
|
||||||
- **S4 · Waits migration, one wait at a time (1–2 wk).** Implement
|
- **S4 · Waits migration, one wait at a time (1–2 wk).** Implement
|
||||||
`wasm_begin_async_wait`/`wasm_yield_until`/`wasm_resolve_wait` (13 §2). Order, lowest-risk
|
`wasm_begin_async_wait`/`wasm_yield_until`/`wasm_resolve_wait` (13 §2). Order, lowest-risk
|
||||||
first, each sub-step flipping its own specs (§3b/3c) and deleting its own pump:
|
first, each sub-step flipping its own specs (§3b/3c) and deleting its own pump:
|
||||||
|
|
|
||||||
157
web/standalone/src/wasm/scheduler-shim.test.ts
Normal file
157
web/standalone/src/wasm/scheduler-shim.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
/**
|
||||||
|
* N5 — flood/fairness unit gates for the scheduler shim
|
||||||
|
* (docs/features/async/17 §3d N5; the shim source is scripts/common/shims/
|
||||||
|
* asyncify-scheduler.js, loaded here against a fake runtime surface).
|
||||||
|
*
|
||||||
|
* Doc 06 §starvation: FIFO by default; a stimulus flood must neither reorder
|
||||||
|
* deliveries nor starve them, and the time-boxed pump must not monopolize the
|
||||||
|
* thread in one burst. These are unit gates — the e2e batteries cover the
|
||||||
|
* same machinery under the real runtime.
|
||||||
|
*/
|
||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const SHIM_PATH = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../../../../scripts/common/shims/asyncify-scheduler.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
type SchedulerShape = {
|
||||||
|
mutatorQueue: unknown[];
|
||||||
|
mutatorsDelivered: number;
|
||||||
|
readyWakes: { deliver: (r: unknown) => void; result: unknown }[];
|
||||||
|
deferredWakes: number;
|
||||||
|
drainedWakes: number;
|
||||||
|
strayWrites: number;
|
||||||
|
_openBusy(): boolean;
|
||||||
|
_armMutatorPump(): void;
|
||||||
|
_scheduleWakeDrain(): void;
|
||||||
|
state(): string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __wxScheduler: SchedulerShape | undefined;
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __wxSchedulerInstalled: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShim(opts: { busy: () => boolean }) {
|
||||||
|
// Fresh globals per load — the shim's install guard is per-context.
|
||||||
|
delete (globalThis as Record<string, unknown>).__wxSchedulerInstalled;
|
||||||
|
delete (globalThis as Record<string, unknown>).__wxScheduler;
|
||||||
|
const g = globalThis as Record<string, unknown>;
|
||||||
|
g.Asyncify = {
|
||||||
|
state: 0,
|
||||||
|
exportCallStack: [],
|
||||||
|
currData: null,
|
||||||
|
handleSleep: function (startAsync: (wake: (r: unknown) => void) => void) {
|
||||||
|
startAsync(() => undefined);
|
||||||
|
},
|
||||||
|
allocateData: () => 0,
|
||||||
|
maybeStopUnwind: () => undefined,
|
||||||
|
};
|
||||||
|
g.Module = {
|
||||||
|
kicadOpenFileBusy: opts.busy,
|
||||||
|
kicadCollabApplyItems: (x: unknown) => `applied:${String(x)}`,
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
(0, eval)(readFileSync(SHIM_PATH, "utf8"));
|
||||||
|
const S = (globalThis as Record<string, unknown>).__wxScheduler as SchedulerShape;
|
||||||
|
// Run the Module init hook (fake runtime: pretend init fired).
|
||||||
|
const M = g.Module as { onRuntimeInitialized?: () => void };
|
||||||
|
M.onRuntimeInitialized?.();
|
||||||
|
return S;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("N5: scheduler shim under flood", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("500-call mutator flood delivers strictly FIFO with zero drops", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
let busy = true;
|
||||||
|
const S = loadShim({ busy: () => busy });
|
||||||
|
const M = (globalThis as Record<string, unknown>).Module as {
|
||||||
|
kicadCollabApplyItems: (x: number) => Promise<string> | string;
|
||||||
|
};
|
||||||
|
const N = 500;
|
||||||
|
const results: Promise<string>[] = [];
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
results.push(Promise.resolve(M.kicadCollabApplyItems(i)));
|
||||||
|
}
|
||||||
|
expect(S.mutatorQueue.length).toBe(N);
|
||||||
|
|
||||||
|
busy = false;
|
||||||
|
// Drive fake time until the queue drains (the pump self-rearms at 16ms).
|
||||||
|
for (let guard = 0; guard < 5000 && S.mutatorQueue.length > 0; guard++) {
|
||||||
|
await vi.advanceTimersByTimeAsync(16);
|
||||||
|
}
|
||||||
|
expect(S.mutatorQueue.length, "flood fully drained (no starvation)").toBe(0);
|
||||||
|
|
||||||
|
const values = await Promise.all(results);
|
||||||
|
// Strict FIFO: promise i resolved with its own payload, in order.
|
||||||
|
for (let i = 0; i < N; i++) expect(values[i]).toBe(`applied:${i}`);
|
||||||
|
expect(S.mutatorsDelivered).toBe(N);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("time-boxed pump yields between chunks — a flood cannot monopolize one task", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
let busy = true;
|
||||||
|
const S = loadShim({ busy: () => busy });
|
||||||
|
const M = (globalThis as Record<string, unknown>).Module as {
|
||||||
|
kicadCollabApplyItems: (x: number) => unknown;
|
||||||
|
};
|
||||||
|
// Each delivery burns ~1ms of fake "work" — with an 8ms box, one tick
|
||||||
|
// must deliver only a handful, not all 100.
|
||||||
|
const realNow = performance.now.bind(performance);
|
||||||
|
let clock = 0;
|
||||||
|
const nowSpy = vi.spyOn(performance, "now").mockImplementation(() => {
|
||||||
|
clock += 1; // every now() call advances 1ms: ~8 deliveries per box
|
||||||
|
return clock;
|
||||||
|
});
|
||||||
|
for (let i = 0; i < 100; i++) void M.kicadCollabApplyItems(i);
|
||||||
|
busy = false;
|
||||||
|
await vi.advanceTimersByTimeAsync(16); // exactly one pump tick
|
||||||
|
const afterOneTick = 100 - S.mutatorQueue.length;
|
||||||
|
expect(afterOneTick, "one tick delivered something").toBeGreaterThan(0);
|
||||||
|
expect(afterOneTick, "one tick did NOT deliver the whole flood").toBeLessThan(100);
|
||||||
|
nowSpy.mockRestore();
|
||||||
|
void realNow;
|
||||||
|
// And the remainder still drains (no starvation after the box closes).
|
||||||
|
for (let guard = 0; guard < 200 && S.mutatorQueue.length > 0; guard++) {
|
||||||
|
await vi.advanceTimersByTimeAsync(16);
|
||||||
|
}
|
||||||
|
expect(S.mutatorQueue.length).toBe(0);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deferred wakes drain strictly FIFO", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const S = loadShim({ busy: () => false });
|
||||||
|
const order: number[] = [];
|
||||||
|
// Queue 50 deferred wakes directly (the runtime path queues these when
|
||||||
|
// a wake arrives mid-transition); the drain must preserve order.
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
S.readyWakes.push({ deliver: (r) => order.push(r as number), result: i });
|
||||||
|
}
|
||||||
|
S._scheduleWakeDrain();
|
||||||
|
await vi.advanceTimersByTimeAsync(50);
|
||||||
|
expect(order).toEqual(Array.from({ length: 50 }, (_, i) => i));
|
||||||
|
expect(S.readyWakes.length).toBe(0);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 45563434a2e23b513f0b71e2e9ad5ab6a8bcae00
|
Subproject commit 6c33cafa9c31fba99d4dae4b0d300183751d05dd
|
||||||
Loading…
Reference in a new issue