jspi: retire the asyncify pipeline — knob, post-link tail, binaryen hooks
Phase 8 in the parent repo. Deleted: asyncify-scheduler.js, apply-asyncify.sh, apply-finalize.sh, inject-dyncall-shims.sh, asyncify-imports/removelist.txt, the wasm-opt/finalize stub pair, scripts/binaryen-hoist-pass/ (the fork stays a dormant submodule; removal is a follow-up), bench/wasm-opt-bench.sh (README marked historical), wasm/shims/context_sleep.cpp, and the sched-context harness app + Makefile targets. PCBJAM_ASYNC_BACKEND is gone: build-wx-wasm.sh hardcodes the jspi stamp (still force-cleans pre-migration trees), build-kicad-target.sh gives editors the JSPI link surface and the CLIs nothing (they pin ASYNCIFY=0), the stub dance is replaced by an unconditional .real-restore, build-wasm-test.sh lost its whole post-link loop, docker/build.sh's postprocess is the ENV shim only, and Makefile.wasm links every app JSPI with the scheduler shim as a tracked prerequisite. pcbjam_async_policy.h keys on __EMSCRIPTEN__. jspi-scheduler.js: wxWasmMainLoopPump dropped from the wrap census (the export died with the D5 detach); inert [TRACE] instrumentation removed. CI: wasm-build.yml rewritten for the single-cache pipeline (one output cache keyed on compile inputs; post-processed bytes cached after the shim); opt_level input removed from both callers. wasm-cache-hash.mjs inputs now cover patch-env-shim.mjs + jspi-scheduler.js + jspi-exports.txt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
This commit is contained in:
parent
3f09a46ff5
commit
e14faeca8b
36 changed files with 170 additions and 3348 deletions
|
|
@ -1,801 +0,0 @@
|
|||
// === AsyncifyScheduler (S2 — scheduler core: registry, deferred wakes, single writer) ===
|
||||
// __WX_SCHEDULER_SHIM_SOURCE__ — injector idempotence sentinel. Must appear ONLY in
|
||||
// this file: the obvious marker (__wxSchedulerInstalled) also occurs in evtloop.cpp's
|
||||
// EM_JS probe text inside every glue, which made the injector skip real injections.
|
||||
// docs/features/async/17-mailbox-scheduler-plan.md · injected only on WX_SCHEDULER=1 builds.
|
||||
//
|
||||
// S2 state: this file REPLACES the legacy handlesleep.js on scheduler builds (the
|
||||
// injector's either-or flip). It carries:
|
||||
// S1 · the wx mailbox (timer/wheel messages, wx/wasm/private/mailbox.h) and the
|
||||
// embind mutator lane (doc 18 classification).
|
||||
// S2 · the scheduler core — every behavior the legacy shim provided (per-sleep
|
||||
// currData capture/restore, the stale-fiber consume-once/quarantine guard,
|
||||
// wake-window flags, flight recorder, trampoline heal) PLUS:
|
||||
// - DEFERRED WAKES: a sleep wake arriving while a transition is in flight
|
||||
// (state != Normal, or the fiber trampoline mid-loop) is queued and
|
||||
// delivered from a clean macrotask when the slot frees — the aliased-wake
|
||||
// class becomes unrepresentable instead of merely detected (doc 12 §law).
|
||||
// - N1 SINGLE-WRITER TRIPWIRE: Asyncify.currData is an accessor; a write
|
||||
// from pure JS (no wasm frames on the export stack) without scheduler
|
||||
// authorization is a STRAY — beaconed, counted, and (opt-in strict mode)
|
||||
// thrown. Wasm-driven writes (fiber_swap, handleSleep internals) are
|
||||
// runtime-legitimate and pass through.
|
||||
// Contract surfaces kept name-identical (external readers!): Asyncify.__wakingRoot
|
||||
// (libcontext EM_JS wasm_root_wake_in_flight), __inSleepWake, __wakingOwnerFiber,
|
||||
// __pendingSleepContexts, Fibers.__fcsTotal/__rootHotTotal/__rootFiber/
|
||||
// __validSuspensions/__internallyParked/__parkSleepBuf/__inFiberEntry,
|
||||
// the "[wx-asyncify] STATE"/"RECORDER" dump formats, window.__wxAsyncifyDump.
|
||||
if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||
globalThis.__wxSchedulerInstalled = true;
|
||||
Asyncify.__schedulerBuild = 1;
|
||||
|
||||
var AsyncifyScheduler = {
|
||||
// --- S1 wx mailbox ----------------------------------------------------
|
||||
mailbox: [],
|
||||
enqueued: 0,
|
||||
delivered: 0,
|
||||
_tickArmed: false,
|
||||
enqueueAfter: function (fn, arg, ms) {
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
if (self.dead) return; // S6: never deliver into a torn-down app
|
||||
self.mailbox.push({ fn: fn, arg: arg });
|
||||
self.enqueued++;
|
||||
self._armDeliveryTick();
|
||||
}, ms);
|
||||
},
|
||||
pop: function () {
|
||||
var m = this.mailbox.shift();
|
||||
if (m) this.delivered++;
|
||||
return m || null;
|
||||
},
|
||||
// Deliver via a dedicated PLAIN export call (wxWasmMailboxTick), never
|
||||
// from inside a pump's awaited ProcessEvents ccall — a fiber swap there
|
||||
// sits on the JS-awaits-a-suspending-export boundary (#13302) and traps.
|
||||
// Resume ready contexts from a FRESH task. Separate from the mailbox tick
|
||||
// because it must run even when the mailbox is empty: a context wake is
|
||||
// work the pump owns, not a queued message.
|
||||
_armSchedPump: function () {
|
||||
if (this._pumpArmed) return;
|
||||
this._pumpArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self._pumpArmed = false;
|
||||
if (self.dead) return;
|
||||
try {
|
||||
if (Module["_wxWasmSchedPump"]) Module["_wxWasmSchedPump"]();
|
||||
} catch (e) {
|
||||
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
|
||||
// The exception escaped a context through drain()'s fiber swap, so
|
||||
// the transition it started never completed: without this every
|
||||
// later pump refuses ("transition in flight") and all outstanding
|
||||
// waits stall forever (doc 22 Phase B).
|
||||
if (Module["_wxWasmSchedAbandon"]) Module["_wxWasmSchedAbandon"]();
|
||||
throw e;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
_armDeliveryTick: function () {
|
||||
if (this._tickArmed) return;
|
||||
this._tickArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function tick() {
|
||||
if (self.dead) { self._tickArmed = false; return; }
|
||||
try {
|
||||
if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"]();
|
||||
} catch (e) {
|
||||
self._tickArmed = false;
|
||||
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
|
||||
// Same containment as the top-level tick's error path (evtloop.cpp):
|
||||
// a throwing handler must not leave a parked quasi-modal unresolved.
|
||||
// No-ops when no such wait is open (5101 = wxID_CANCEL).
|
||||
self.resolveTopWait('nested', 0);
|
||||
self.resolveTopWait('modal', 5101);
|
||||
throw e;
|
||||
}
|
||||
if (self.mailbox.length > 0) {
|
||||
setTimeout(tick, 17);
|
||||
} else {
|
||||
self._tickArmed = false;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
// --- S1 embind lane ---------------------------------------------------
|
||||
MUTATOR_NAMES: [
|
||||
"kicadSetChrome", "kicadSetReadOnly",
|
||||
"kicadCollabApply", "kicadCollabApplyItems",
|
||||
"kicadCollabSnapshot", "kicadCollabSnapshotItems",
|
||||
"kicadCollabPresenceStart", "kicadCollabSetRemote",
|
||||
"kicadCollabSetPins", "kicadCollabSetStyle",
|
||||
"kicadCollabSetViewport", "kicadCollabFitViewport",
|
||||
"kicadCollabReleaseSelection", "kicadSetColorTheme",
|
||||
"kicadSaveBoard", "kicadSaveSchematic", "kicadSaveDrawingSheet",
|
||||
],
|
||||
mutatorQueue: [],
|
||||
mutatorsWrapped: 0,
|
||||
mutatorsDelivered: 0,
|
||||
_mutatorPumpArmed: false,
|
||||
_openBusy: function () {
|
||||
var probe = Module["kicadOpenFileBusy"];
|
||||
if (typeof probe !== "function") return false;
|
||||
try { return !!probe(); } catch (e) { return true; }
|
||||
},
|
||||
_wrapMutators: function () {
|
||||
var self = this;
|
||||
this.MUTATOR_NAMES.forEach(function (name) {
|
||||
var orig = Module[name];
|
||||
if (typeof orig !== "function") return;
|
||||
self.mutatorsWrapped++;
|
||||
Module[name] = function () {
|
||||
var args = arguments;
|
||||
var call = function () { return orig.apply(Module, args); };
|
||||
if (self.mutatorQueue.length === 0 && !self._openBusy()) {
|
||||
self.mutatorsDelivered++;
|
||||
return call();
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.mutatorQueue.push({ name: name, call: call, resolve: resolve, reject: reject });
|
||||
self._armMutatorPump();
|
||||
});
|
||||
};
|
||||
});
|
||||
if (this.mutatorsWrapped > 0)
|
||||
console.log("[wx-scheduler] embind lane: wrapped " + this.mutatorsWrapped + " mutator(s)");
|
||||
},
|
||||
// Phase F (doc 22 §10, the awaited-ccall entry class): when the binary
|
||||
// carries kicadOpenFileStart, the open body runs on a DISPATCH CONTEXT
|
||||
// and the await surface becomes a plain JS promise over the wait token —
|
||||
// the main stack never parks in place during a load. Conditional: older
|
||||
// binaries without the starter keep the legacy suspending export.
|
||||
_wrapOpenFile: function () {
|
||||
var self = this;
|
||||
var start = Module["kicadOpenFileStart"];
|
||||
if (typeof start !== "function" || typeof Module["kicadOpenFile"] !== "function") return;
|
||||
Module["kicadOpenFile"] = function (path) {
|
||||
// Mint the token HERE, in pure JS — the starter runs the load on a
|
||||
// dispatch context and Asyncify-suspends its own frame, so a token
|
||||
// RETURNED from it would arrive as a placeholder (0). We own the token
|
||||
// and await its promise; the job resolves it when the load finishes.
|
||||
var token = self.beginWait("open");
|
||||
start(token, path);
|
||||
// waitPromise consumes early-resolved entries (the fast-error path),
|
||||
// so a job that finished before this await still resolves correctly.
|
||||
return self.waitPromise(token).then(function (r) { return !!r; });
|
||||
};
|
||||
console.log("[wx-scheduler] open lane: kicadOpenFile routed through the dispatch context");
|
||||
},
|
||||
_armMutatorPump: function () {
|
||||
if (this._mutatorPumpArmed) return;
|
||||
this._mutatorPumpArmed = true;
|
||||
var self = this;
|
||||
var now = (typeof performance !== "undefined" && performance.now)
|
||||
? function () { return performance.now(); }
|
||||
: function () { return Date.now(); };
|
||||
setTimeout(function pump() {
|
||||
if (self.dead) { self._mutatorPumpArmed = false; return; }
|
||||
// Unkillable: an exception escaping this body would end the setTimeout
|
||||
// chain and wedge the queue forever (observed: 559 frozen messages).
|
||||
try {
|
||||
if (!self._openBusy()) {
|
||||
// Time-boxed drain: ~8 ms of work per 16 ms tick keeps the page
|
||||
// live while a long backlog drains in order.
|
||||
var t0 = now();
|
||||
while (self.mutatorQueue.length > 0 && now() - t0 < 8) {
|
||||
if (self._openBusy()) break;
|
||||
var m = self.mutatorQueue.shift();
|
||||
self.mutatorsDelivered++;
|
||||
try { m.resolve(m.call()); } catch (e) { m.reject(e); }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
self._pumpErrors = (self._pumpErrors || 0) + 1;
|
||||
if (self._pumpErrors <= 5)
|
||||
console.warn("[wx-scheduler] mutator pump error (occurrence "
|
||||
+ self._pumpErrors + "): " + e);
|
||||
}
|
||||
if (self.mutatorQueue.length > 0) setTimeout(pump, 16);
|
||||
else self._mutatorPumpArmed = false;
|
||||
}, 16);
|
||||
},
|
||||
|
||||
// --- S4 wait registry ---------------------------------------------------
|
||||
// Token-based waits (doc 13 §2: wasm_begin_async_wait / wasm_yield_until /
|
||||
// wasm_resolve_wait). A wait is begun BEFORE the C++ side parks, so a
|
||||
// resolve that races ahead of the park (EndModal during Show()) simply
|
||||
// pre-resolves the promise — yieldUntil then returns immediately. Per-kind
|
||||
// LIFO stacks give wx modal/nested semantics ("innermost first") without
|
||||
// the legacy per-wait resolver stacks (_wxModalResolvers /
|
||||
// _wxNestedLoopExit — deleted at doc 20 D-1). Resolution flows
|
||||
// through the S2 deferred-wake law automatically: resolving a wait wakes
|
||||
// its parked sleep via the wrapped handleSleep path.
|
||||
waits: new Map(), // token → {kind, promise, resolve, resolved, result, awaited, contextParked}
|
||||
waitSeq: 0,
|
||||
waitStacks: {}, // kind → [unresolved tokens], LIFO
|
||||
waitsBegun: 0,
|
||||
waitsResolved: 0,
|
||||
earlyWaitResolves: 0, // resolves that landed before their waiter parked (Phase E)
|
||||
beginWait: function (kind) {
|
||||
var token = ++this.waitSeq;
|
||||
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
|
||||
var self = this;
|
||||
entry.promise = new Promise(function (resolve) { entry.resolve = resolve; });
|
||||
this.waits.set(token, entry);
|
||||
(this.waitStacks[kind] = this.waitStacks[kind] || []).push(token);
|
||||
this.waitsBegun++;
|
||||
return token;
|
||||
},
|
||||
waitPromise: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry) {
|
||||
console.warn("[wx-scheduler] waitPromise(" + token + "): unknown token");
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
if (entry.resolved) {
|
||||
// Resolved before the waiter parked (Phase E early-resolve window).
|
||||
// Consume the retained entry and hand the real result over — the old
|
||||
// path warned "unknown token" and returned 0, dropping it.
|
||||
this.waits.delete(token);
|
||||
return Promise.resolve(entry.result | 0);
|
||||
}
|
||||
entry.awaited = true;
|
||||
return entry.promise;
|
||||
},
|
||||
// Phase E: a wait resolved before its C++ waiter reached the park keeps its
|
||||
// entry (see resolveWait) so the result is not lost. wxWasmYieldUntil peeks
|
||||
// before parking a context and consumes the result instead of parking a
|
||||
// context nobody will ever resume.
|
||||
waitEarlyResolved: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
return entry && entry.resolved ? 1 : 0;
|
||||
},
|
||||
takeWaitResult: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || !entry.resolved) return 0;
|
||||
this.waits.delete(token);
|
||||
return entry.result | 0;
|
||||
},
|
||||
// doc 22 Phase C: this token's waiter parked a SCHEDULER CONTEXT instead of
|
||||
// suspending its stack in place, so there is no promise anyone awaits —
|
||||
// resolving one would strand the context forever. Marked from C++ at park
|
||||
// time; resolveWait routes such tokens to the registry instead.
|
||||
noteContextWait: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (entry) entry.contextParked = true;
|
||||
},
|
||||
resolveWait: function (token, result) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || entry.resolved) return false;
|
||||
entry.resolved = true;
|
||||
this.waitsResolved++;
|
||||
var stack = this.waitStacks[entry.kind];
|
||||
if (stack) {
|
||||
var idx = stack.indexOf(token);
|
||||
if (idx !== -1) stack.splice(idx, 1);
|
||||
}
|
||||
if (entry.contextParked) {
|
||||
// Mark ready only — never resume inline. The pump picks it up from a
|
||||
// fresh task, which is doc 13 §1.4's deferred-wake law applied to
|
||||
// contexts (a rewind inside this resolver's own turn is the whole
|
||||
// class of bug the scheduler exists to remove).
|
||||
this.waits.delete(token);
|
||||
try {
|
||||
Module["_wxWasmSchedResolveContextWait"](token, result | 0);
|
||||
} catch (e) {
|
||||
console.warn("[wx-scheduler] context wait " + token + " resolve failed: " + e);
|
||||
}
|
||||
this._armSchedPump();
|
||||
return true;
|
||||
}
|
||||
entry.result = result | 0;
|
||||
entry.resolve(result | 0);
|
||||
if (entry.awaited) {
|
||||
this.waits.delete(token);
|
||||
} else {
|
||||
// Nobody has parked on this token yet (Phase E early-resolve window:
|
||||
// a bridge whose request settled before the C++ frame reached the
|
||||
// park). Keep the entry, result attached — wxWasmYieldUntil or a late
|
||||
// waitPromise consumes it. Deleting here is what stranded the first
|
||||
// Phase E attempt: the later park waited on a wake nobody could send.
|
||||
this.earlyWaitResolves++;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics).
|
||||
resolveTopWait: function (kind, result) {
|
||||
var stack = this.waitStacks[kind];
|
||||
if (!stack || stack.length === 0) return false;
|
||||
return this.resolveWait(stack[stack.length - 1], result);
|
||||
},
|
||||
pendingWaits: function (kind) {
|
||||
var stack = this.waitStacks[kind];
|
||||
return stack ? stack.length : 0;
|
||||
},
|
||||
|
||||
// --- S2 scheduler core state -------------------------------------------
|
||||
// Deferred sleep wakes: {deliver, result} queued because a transition was
|
||||
// in flight when the wake arrived. Delivered FIFO from a clean macrotask.
|
||||
readyWakes: [],
|
||||
deferredWakes: 0,
|
||||
drainedWakes: 0,
|
||||
_wakeDrainArmed: false,
|
||||
// N1: pure-JS currData writes seen without scheduler authorization.
|
||||
strayWrites: 0,
|
||||
// Phase E: fresh in-place Asyncify parks that began on a NON-main stack
|
||||
// (tool coroutine / scheduler context). Must be ZERO at the flip.
|
||||
inplaceParksOnFiberStack: 0,
|
||||
strictStrays: false, // tests set true → stray throws instead of beaconing
|
||||
_authorizedWrite: 0,
|
||||
authorize: function (fn) {
|
||||
this._authorizedWrite++;
|
||||
try { return fn(); } finally { this._authorizedWrite--; }
|
||||
},
|
||||
|
||||
// --- S6 lifetime --------------------------------------------------------
|
||||
// Called when the wx main loop exits (DoRun's top-level return path). The
|
||||
// app object is about to be destroyed: delivering anything after this
|
||||
// point runs callbacks into freed C++ state. Queued mutators reject,
|
||||
// queued messages and wakes drop — loudly, so a teardown that strands
|
||||
// work is visible in the console instead of surfacing as a later UAF.
|
||||
dead: false,
|
||||
shutdown: function (reason) {
|
||||
if (this.dead) return;
|
||||
this.dead = true;
|
||||
var stranded = {
|
||||
mailbox: this.mailbox.length,
|
||||
mutators: this.mutatorQueue.length,
|
||||
wakes: this.readyWakes.length,
|
||||
waits: this.waits.size,
|
||||
};
|
||||
this.mailbox.length = 0;
|
||||
for (var i = 0; i < this.mutatorQueue.length; i++) {
|
||||
try { this.mutatorQueue[i].reject(new Error("[wx-scheduler] shutdown: " + reason)); } catch (e) {}
|
||||
}
|
||||
this.mutatorQueue.length = 0;
|
||||
this.readyWakes.length = 0;
|
||||
if (stranded.mailbox || stranded.mutators || stranded.wakes || stranded.waits) {
|
||||
console.warn("[wx-scheduler] shutdown (" + reason + ") stranded:"
|
||||
+ " mailbox=" + stranded.mailbox
|
||||
+ " mutators=" + stranded.mutators
|
||||
+ " wakes=" + stranded.wakes
|
||||
+ " pendingWaits=" + stranded.waits);
|
||||
} else {
|
||||
console.log("[wx-scheduler] shutdown (" + reason + ") clean");
|
||||
}
|
||||
},
|
||||
|
||||
state: function () {
|
||||
return "[wx-scheduler] build=1 impl=S2-core"
|
||||
+ (this.dead ? " DEAD" : "")
|
||||
+ " mailbox=" + this.mailbox.length
|
||||
+ " enqueued=" + this.enqueued
|
||||
+ " delivered=" + this.delivered
|
||||
+ " mutQ=" + this.mutatorQueue.length
|
||||
+ " mutWrapped=" + this.mutatorsWrapped
|
||||
+ " mutDelivered=" + this.mutatorsDelivered
|
||||
+ " readyWakes=" + this.readyWakes.length
|
||||
+ " deferredWakes=" + this.deferredWakes
|
||||
+ " drainedWakes=" + this.drainedWakes
|
||||
+ " strayWrites=" + this.strayWrites
|
||||
+ " waits=" + this.waits.size
|
||||
+ " waitsBegun=" + this.waitsBegun
|
||||
+ " waitsResolved=" + this.waitsResolved
|
||||
+ " earlyWaitResolves=" + this.earlyWaitResolves
|
||||
+ " fiberStackParks=" + this.inplaceParksOnFiberStack;
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.__wxScheduler = AsyncifyScheduler;
|
||||
|
||||
// ======================================================================
|
||||
// S2 core install. Skipped defensively if the legacy shim somehow got in
|
||||
// first — double-managing the wake path corrupts (the injector's either-or
|
||||
// flip should make this unreachable).
|
||||
// ======================================================================
|
||||
if (Asyncify.__nestedHandleSleepInstalled) {
|
||||
console.warn("[wx-scheduler] legacy handlesleep present - S2 core NOT installed (dual-management guard)");
|
||||
} else if (typeof Asyncify.handleSleep === "function"
|
||||
&& typeof Asyncify.allocateData === "function") {
|
||||
|
||||
// --- flight recorder + state dump (ported verbatim-in-spirit from
|
||||
// handlesleep.js; formats are parsed by guard-beacons.ts and
|
||||
// apps/tests/tools/repro-board-load.ts — do not change shapes) ---------
|
||||
Asyncify.__pendingSleepContexts = [];
|
||||
var __recMax = 96;
|
||||
Asyncify.__rec = [];
|
||||
var __rec = function (ev) {
|
||||
var r = Asyncify.__rec;
|
||||
r.push(((typeof performance !== "undefined" ? performance.now() : 0) | 0) + " " + ev);
|
||||
if (r.length > __recMax) r.shift();
|
||||
};
|
||||
Asyncify.__recPush = __rec;
|
||||
|
||||
var __dumpState = function () {
|
||||
var F = (typeof Fibers !== "undefined") ? Fibers : null;
|
||||
var pend = Array.isArray(Asyncify.__pendingSleepContexts)
|
||||
? Asyncify.__pendingSleepContexts.map(function (c) { return c.capturedData || 0; }).join(",")
|
||||
: "n/a";
|
||||
var head = "[wx-asyncify] STATE"
|
||||
+ " state=" + Asyncify.state
|
||||
+ " currData=" + (Asyncify.currData || 0)
|
||||
+ " inSleepWake=" + (Asyncify.__inSleepWake || 0)
|
||||
+ " exportStack=" + (Asyncify.exportCallStack ? Asyncify.exportCallStack.length : -1)
|
||||
+ " pendingSleeps=[" + pend + "]"
|
||||
+ (F ? (" nextFiber=" + F.nextFiber
|
||||
+ " trampolining=" + F.trampolineRunning
|
||||
+ " root=" + F.__rootFiber
|
||||
+ " fcsTotal=" + (F.__fcsTotal || 0)
|
||||
+ " rootHotTotal=" + (F.__rootHotTotal || 0)
|
||||
+ " valid=[" + (F.__validSuspensions ? Array.from(F.__validSuspensions).join(",") : "") + "]"
|
||||
+ " parked=[" + (F.__internallyParked ? Array.from(F.__internallyParked).join(",") : "") + "]"
|
||||
+ " deferrals=" + (F.__rootDeferrals || 0))
|
||||
: " (no Fibers)")
|
||||
+ " | " + AsyncifyScheduler.state();
|
||||
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify.__rec.join("\n ");
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
window.__wxAsyncifyDump = __dumpState;
|
||||
var __dumps = 0;
|
||||
var __onTrap = function (msg) {
|
||||
if (__dumps >= 2) return;
|
||||
if (!/index out of bounds|unreachable executed|table index|indirect call signature|null function or function signature|memory access out of bounds/i.test(msg)) return;
|
||||
++__dumps;
|
||||
try { console.error(__dumpState()); } catch (e) {}
|
||||
};
|
||||
window.addEventListener("error", function (e) {
|
||||
__onTrap(e && e.error instanceof Error ? e.error.message : String((e && e.message) || ""));
|
||||
});
|
||||
window.addEventListener("unhandledrejection", function (e) {
|
||||
__onTrap(e && e.reason instanceof Error ? e.reason.message : String((e && e.reason) || ""));
|
||||
});
|
||||
}
|
||||
|
||||
var __wxAsyncifyReport = (function () {
|
||||
var counts = {};
|
||||
return function (kind, msg, withStack) {
|
||||
var n = (counts[kind] = (counts[kind] || 0) + 1);
|
||||
if (n > 10 && n % 100 !== 0) return;
|
||||
var line = "[wx-asyncify] " + kind + ": " + msg + " (occurrence " + n + ")";
|
||||
if (withStack) {
|
||||
try { line += "\n" + String(new Error().stack).split("\n").slice(1, 8).join("\n"); } catch (e) {}
|
||||
}
|
||||
console.warn(line);
|
||||
};
|
||||
})();
|
||||
|
||||
// --- N1: single-writer accessor on Asyncify.currData ------------------
|
||||
// Writes made while compiled code is on the export stack are the wasm
|
||||
// runtime's own (fiber_swap, handleSleep's park/stop paths) — legitimate.
|
||||
// A pure-JS write (empty export stack) must come from a scheduler-
|
||||
// authorized span; anything else is a STRAY: the exact shape of every
|
||||
// historical corruption's bad write. Beacon + count; strict mode throws.
|
||||
(function () {
|
||||
var realCurrData = Asyncify.currData; // null at install time
|
||||
Object.defineProperty(Asyncify, "currData", {
|
||||
configurable: true,
|
||||
get: function () { return realCurrData; },
|
||||
set: function (v) {
|
||||
if ((!Asyncify.exportCallStack || Asyncify.exportCallStack.length === 0)
|
||||
&& AsyncifyScheduler._authorizedWrite === 0
|
||||
&& !(typeof Fibers !== "undefined" && Fibers.trampolineRunning)) {
|
||||
AsyncifyScheduler.strayWrites++;
|
||||
__wxAsyncifyReport("stray-currdata-write",
|
||||
"currData=" + (v || 0) + " written from pure JS without scheduler authorization", true);
|
||||
if (AsyncifyScheduler.strictStrays)
|
||||
throw new Error("[wx-scheduler] stray currData write (strict mode)");
|
||||
}
|
||||
realCurrData = v;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
// --- deferred-wake drain ----------------------------------------------
|
||||
var __transitionFree = function () {
|
||||
return Asyncify.state === 0
|
||||
&& !(typeof Fibers !== "undefined" && Fibers.trampolineRunning);
|
||||
};
|
||||
AsyncifyScheduler._scheduleWakeDrain = function () {
|
||||
if (this._wakeDrainArmed) return;
|
||||
this._wakeDrainArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self._wakeDrainArmed = false;
|
||||
if (self.dead) return; // S6: parked stacks are gone with the app
|
||||
// Deliver from a CLEAN macrotask (export stack empty by construction).
|
||||
while (self.readyWakes.length > 0 && __transitionFree()) {
|
||||
var w = self.readyWakes.shift();
|
||||
self.drainedWakes++;
|
||||
w.deliver(w.result);
|
||||
}
|
||||
if (self.readyWakes.length > 0) self._scheduleWakeDrain();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// --- handleSleep wrap: registry + capture/restore + deferral ----------
|
||||
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
|
||||
Asyncify.allocateData = function () {
|
||||
var ptr = __originalAllocateData();
|
||||
for (var i = Asyncify.__pendingSleepContexts.length - 1; i >= 0; --i) {
|
||||
var ctx = Asyncify.__pendingSleepContexts[i];
|
||||
if (!ctx.capturedData) {
|
||||
ctx.capturedData = ptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
var __originalHandleSleep = Asyncify.handleSleep.bind(Asyncify);
|
||||
Asyncify.handleSleep = function (startAsync) {
|
||||
__rec("sleep s=" + Asyncify.state + " cd=" + (Asyncify.currData || 0)
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||
if (Asyncify.state === 0 && Asyncify.currData) {
|
||||
__wxAsyncifyReport("concurrent-park",
|
||||
"handleSleep entered while currData=" + Asyncify.currData, true);
|
||||
}
|
||||
if (Asyncify.state === 1) {
|
||||
__wxAsyncifyReport("reentrant-state",
|
||||
"handleSleep entered mid-unwind (state=1) currData=" + Asyncify.currData, true);
|
||||
}
|
||||
// Only a FRESH park (state 0) allocates data and needs tracking; the
|
||||
// state-2 resume re-entry returns synchronously through the rewind
|
||||
// branch (a context pushed for it leaks one per resume).
|
||||
if (Asyncify.state !== 0) {
|
||||
return __originalHandleSleep(startAsync);
|
||||
}
|
||||
// Phase F (doc 22 §10 F2/F3): report every fresh in-place park to the
|
||||
// REGISTRY. Begin() returns the owning context id (0 = main stack);
|
||||
// while recorded, fiber_enterable()/fiber_transfer refuse entering that
|
||||
// context — the registry-owned replacement for the deleted quarantine.
|
||||
// Also the Phase E telemetry: fiberStackParks must be 0 at the flip's
|
||||
// repro gate. Leaf probe into wasm; state is 0 here so no unwind is in
|
||||
// flight yet.
|
||||
var parkOwnerCtx = 0;
|
||||
try {
|
||||
if (Module["_wxWasmSchedInplaceParkBegin"]) {
|
||||
parkOwnerCtx = Module["_wxWasmSchedInplaceParkBegin"]() | 0;
|
||||
if (parkOwnerCtx) {
|
||||
AsyncifyScheduler.inplaceParksOnFiberStack++;
|
||||
__rec("inplace-park-on-fiber-stack ctx=" + parkOwnerCtx
|
||||
+ " n=" + AsyncifyScheduler.inplaceParksOnFiberStack);
|
||||
}
|
||||
}
|
||||
} catch (e) { /* probe must never break a park */ }
|
||||
var sleepCtx = {
|
||||
capturedData: null,
|
||||
cleanedUp: false,
|
||||
parkOwnerCtx: parkOwnerCtx,
|
||||
rootOwned: (typeof Fibers === "undefined")
|
||||
|| (!Fibers.__inFiberEntry
|
||||
&& !(Asyncify.__wakingOwnerFiber || false)),
|
||||
};
|
||||
Asyncify.__pendingSleepContexts.push(sleepCtx);
|
||||
|
||||
var cleanup = function () {
|
||||
if (sleepCtx.cleanedUp) return;
|
||||
sleepCtx.cleanedUp = true;
|
||||
if (sleepCtx.parkOwnerCtx) {
|
||||
try {
|
||||
if (Module["_wxWasmSchedInplaceParkEnd"]) {
|
||||
Module["_wxWasmSchedInplaceParkEnd"](sleepCtx.parkOwnerCtx);
|
||||
}
|
||||
} catch (e) { /* never break a wake */ }
|
||||
sleepCtx.parkOwnerCtx = 0;
|
||||
}
|
||||
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
|
||||
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
|
||||
};
|
||||
|
||||
try {
|
||||
return __originalHandleSleep(function (wakeUp) {
|
||||
// deliver(): the legacy shim's whole wake path — restore OUR buffer,
|
||||
// mark the wake window, swallow the "unwind" sentinel.
|
||||
var deliver = function (result) {
|
||||
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0)
|
||||
+ (sleepCtx.rootOwned ? " R" : " f"));
|
||||
if (sleepCtx.capturedData) {
|
||||
if (Asyncify.currData !== sleepCtx.capturedData) {
|
||||
__wxAsyncifyReport(
|
||||
Asyncify.currData ? "aliased-wake-live" : "overlapped-wake",
|
||||
"restoring currData=" + sleepCtx.capturedData +
|
||||
" over " + (Asyncify.currData || "null") +
|
||||
" state=" + Asyncify.state, !!Asyncify.currData);
|
||||
}
|
||||
AsyncifyScheduler.authorize(function () {
|
||||
Asyncify.currData = sleepCtx.capturedData;
|
||||
});
|
||||
}
|
||||
cleanup();
|
||||
Asyncify.__inSleepWake = (Asyncify.__inSleepWake || 0) + 1;
|
||||
var prevWakingOwnerFiber = Asyncify.__wakingOwnerFiber || false;
|
||||
Asyncify.__wakingOwnerFiber = !sleepCtx.rootOwned;
|
||||
var prevWakingRoot = Asyncify.__wakingRoot || 0;
|
||||
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = (Asyncify.__wakingRoot || 0) + 1;
|
||||
try {
|
||||
return wakeUp(result);
|
||||
} catch (e) {
|
||||
if (e === "unwind") return;
|
||||
throw e;
|
||||
} finally {
|
||||
Asyncify.__inSleepWake -= 1;
|
||||
Asyncify.__wakingOwnerFiber = prevWakingOwnerFiber;
|
||||
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = prevWakingRoot;
|
||||
}
|
||||
};
|
||||
return startAsync(function (result) {
|
||||
// THE S2 LAW (doc 12): a wake never starts a rewind while another
|
||||
// transition is in flight — it enqueues and the drain delivers
|
||||
// from a clean macrotask when the slot frees. The legacy shim
|
||||
// could only beacon this window (aliased-wake-live); the
|
||||
// scheduler removes it.
|
||||
if (!__transitionFree()) {
|
||||
AsyncifyScheduler.deferredWakes++;
|
||||
__rec("defer-wake buf=" + (sleepCtx.capturedData || 0)
|
||||
+ " s=" + Asyncify.state);
|
||||
AsyncifyScheduler.readyWakes.push({ deliver: deliver, result: result });
|
||||
AsyncifyScheduler._scheduleWakeDrain();
|
||||
return;
|
||||
}
|
||||
return deliver(result);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Transition-completion signal: maybeStopUnwind is where an unwind
|
||||
// finishes (state → Normal) and the trampoline runs queued fiber
|
||||
// switches. After it settles, deferred wakes may proceed.
|
||||
var __originalMaybeStopUnwind = Asyncify.maybeStopUnwind.bind(Asyncify);
|
||||
Asyncify.maybeStopUnwind = function () {
|
||||
var ret = __originalMaybeStopUnwind();
|
||||
if (AsyncifyScheduler.readyWakes.length > 0 && __transitionFree())
|
||||
AsyncifyScheduler._scheduleWakeDrain();
|
||||
return ret;
|
||||
};
|
||||
|
||||
Asyncify.__nestedHandleSleepInstalled = true; // compat: tools probe this
|
||||
console.log("[wx-scheduler] S2 core installed (deferred wakes + N1 accessor)");
|
||||
}
|
||||
|
||||
// --- stale-fiber-rewind guard (ported from handlesleep.js; semantics
|
||||
// unchanged — these encode the consume-once/quarantine contracts of
|
||||
// docs/features/async/16) + trampoline heal ownership -------------------
|
||||
// Phase F F2/F3 (doc 22 §10, 2026-08-09): deletion was built, measured and
|
||||
// REVERTED. The registry now carries the in-place-park fact
|
||||
// (wxWasmSchedInplaceParkBegin/End) and refuses on the transfer lane, but
|
||||
// the quarantine's DROP is still the only correct recovery for a misrouted
|
||||
// yield-back under attribution rot (a C++-level refusal ghost-resumes the
|
||||
// yielding coroutine — measured as the lever's phase2 overshoot). This
|
||||
// block stays until attribution is registry-authoritative (gap 3).
|
||||
if (typeof Fibers !== "undefined"
|
||||
&& typeof Fibers.finishContextSwitch === "function"
|
||||
&& !Fibers.__staleRewindGuardInstalled) {
|
||||
Fibers.__validSuspensions = new Set();
|
||||
Fibers.__internallyParked = new Set();
|
||||
Fibers.__parkSleepBuf = new Map();
|
||||
|
||||
var __origFinishContextSwitch = Fibers.finishContextSwitch.bind(Fibers);
|
||||
var __fiberRefusals = 0;
|
||||
|
||||
var __fcsRec = (typeof Asyncify !== "undefined" && Asyncify.__recPush)
|
||||
? Asyncify.__recPush
|
||||
: function () {};
|
||||
|
||||
var __refuseFiber = function (newFiber, why) {
|
||||
__fcsRec("refuse new=" + newFiber);
|
||||
++__fiberRefusals;
|
||||
if (__fiberRefusals <= 10 || __fiberRefusals % 100 === 0) {
|
||||
console.warn("[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
|
||||
+ " (occurrence " + __fiberRefusals + ")");
|
||||
}
|
||||
AsyncifyScheduler.authorize(function () {
|
||||
Asyncify.currData = null;
|
||||
});
|
||||
};
|
||||
|
||||
Fibers.finishContextSwitch = function (newFiber) {
|
||||
Fibers.__fcsTotal = (Fibers.__fcsTotal || 0) + 1;
|
||||
if (newFiber === Fibers.__rootFiber && (Asyncify.__inSleepWake || 0) > 0) {
|
||||
Fibers.__rootHotTotal = (Fibers.__rootHotTotal || 0) + 1;
|
||||
}
|
||||
var __remStr = "";
|
||||
if (Asyncify.currData) {
|
||||
var __H = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||
__remStr = " rem=" + (__H[((Asyncify.currData + 4) >>> 2) >>> 0] - __H[(Asyncify.currData >>> 2) >>> 0])
|
||||
+ " rf=" + (Asyncify.getDataRewindFuncName ? Asyncify.getDataRewindFuncName(Asyncify.currData) : "?")
|
||||
+ " es=[" + (Asyncify.exportCallStack || []).join("|") + "]";
|
||||
}
|
||||
__fcsRec("fcs old=" + (Asyncify.currData ? Asyncify.currData - 20 : 0)
|
||||
+ " new=" + newFiber
|
||||
+ (newFiber === Fibers.__rootFiber ? " ROOT" : "")
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0) + __remStr);
|
||||
if (Asyncify.currData) {
|
||||
var oldFiber = Asyncify.currData - 20;
|
||||
if (Fibers.__rootFiber === undefined) {
|
||||
Fibers.__rootFiber = oldFiber;
|
||||
}
|
||||
var parkBuf = Fibers.__parkSleepBuf.get(oldFiber);
|
||||
var stillParked = parkBuf !== undefined
|
||||
&& Array.isArray(Asyncify.__pendingSleepContexts)
|
||||
&& Asyncify.__pendingSleepContexts.some(function (c) { return c.capturedData === parkBuf; });
|
||||
if (!stillParked) {
|
||||
Fibers.__validSuspensions.add(oldFiber);
|
||||
Fibers.__internallyParked.delete(oldFiber);
|
||||
Fibers.__parkSleepBuf.delete(oldFiber);
|
||||
}
|
||||
}
|
||||
|
||||
var isRoot = newFiber === Fibers.__rootFiber;
|
||||
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
||||
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
|
||||
__refuseFiber(newFiber, "is asyncify-parked mid-body (sleep in flight)");
|
||||
return;
|
||||
}
|
||||
if (entryPoint === 0) {
|
||||
if (!Fibers.__validSuspensions.has(newFiber)) {
|
||||
__refuseFiber(newFiber, isRoot
|
||||
? "root suspension already consumed - a second rewind would replay stale frames"
|
||||
: "has no live suspension - rewinding would replay stale data");
|
||||
return;
|
||||
}
|
||||
Fibers.__validSuspensions.delete(newFiber);
|
||||
}
|
||||
|
||||
if (!isRoot) Fibers.__inFiberEntry = (Fibers.__inFiberEntry || 0) + 1;
|
||||
var ret;
|
||||
try {
|
||||
// The original writes currData (entry path nulls it, resume path sets
|
||||
// the fiber's buffer) from pure JS — scheduler-supervised here.
|
||||
ret = AsyncifyScheduler.authorize(function () {
|
||||
return __origFinishContextSwitch(newFiber);
|
||||
});
|
||||
} finally {
|
||||
if (!isRoot) Fibers.__inFiberEntry -= 1;
|
||||
}
|
||||
|
||||
if (!isRoot && !Fibers.nextFiber && Asyncify.currData) {
|
||||
Fibers.__internallyParked.add(newFiber);
|
||||
Fibers.__parkSleepBuf.set(newFiber, Asyncify.currData);
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Trampoline heal ownership (subsumes inject-dyncall-shims §3c): a throw
|
||||
// escaping the trampoline loop must not leave trampolineRunning wedged —
|
||||
// that guard being stuck turns every later fiber swap into a silent no-op.
|
||||
var __origTrampoline = Fibers.trampoline.bind(Fibers);
|
||||
Fibers.trampoline = function () {
|
||||
try {
|
||||
return __origTrampoline();
|
||||
} catch (e) {
|
||||
Fibers.trampolineRunning = false;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
Fibers.__staleRewindGuardInstalled = true;
|
||||
}
|
||||
|
||||
// Wrap the embind mutators once the runtime has registered them.
|
||||
if (typeof Module !== "undefined") {
|
||||
if (Module["calledRun"]) {
|
||||
AsyncifyScheduler._wrapMutators();
|
||||
AsyncifyScheduler._wrapOpenFile();
|
||||
} else {
|
||||
var __wxSchedPrevInit = Module["onRuntimeInitialized"];
|
||||
Module["onRuntimeInitialized"] = function () {
|
||||
if (typeof __wxSchedPrevInit === "function") __wxSchedPrevInit();
|
||||
AsyncifyScheduler._wrapMutators();
|
||||
AsyncifyScheduler._wrapOpenFile();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[wx-scheduler] scaffolding installed (S2, core live)");
|
||||
}
|
||||
// === End AsyncifyScheduler ===
|
||||
|
|
@ -436,7 +436,6 @@
|
|||
entrySp: region.top,
|
||||
suspendedAt: 0
|
||||
};
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] alloc region ' + region.base + ' -> act ' + rec.id + ':' + name);
|
||||
S._actStack.push(rec);
|
||||
S._setSp(region.top);
|
||||
var out;
|
||||
|
|
@ -466,7 +465,6 @@
|
|||
},
|
||||
|
||||
_endActivation: function (rec) {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] end ' + rec.id + ':' + rec.kind + ' live=' + (this._windowLive ? this._windowLive.id : '-'));
|
||||
this._suspended.delete(rec.id);
|
||||
if (this._windowLive === rec) {
|
||||
// completed from a RESUMED window: wasm's epilogue left SP at the
|
||||
|
|
@ -476,7 +474,6 @@
|
|||
if (rec.enclosingSp !== undefined) this._setSp(rec.enclosingSp);
|
||||
}
|
||||
if (rec.region) {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] free region ' + rec.region.base + ' <- act ' + rec.id);
|
||||
this._regionFree(rec.region);
|
||||
rec.region = null;
|
||||
}
|
||||
|
|
@ -485,7 +482,6 @@
|
|||
},
|
||||
|
||||
_pumpResume: function () {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] pump live=' + (this._windowLive ? this._windowLive.id + ':' + this._windowLive.kind : '-') + ' ready=' + this._resumeReady.map(function(e){return e.rec.id + ':' + e.rec.kind;}).join(','));
|
||||
if (this.dead) return;
|
||||
if (this._windowLive) {
|
||||
// Self-heal: an activation that suspended RAW (bypassing the shim)
|
||||
|
|
@ -580,7 +576,6 @@
|
|||
entrySp: S._sp(), suspendedAt: 0, anon: true
|
||||
};
|
||||
}
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] suspend ' + rec.id + ':' + rec.kind + ' ' + kind + '/' + token + ' live=' + (S._windowLive ? S._windowLive.id : '-') + ' stack=' + S._actStack.map(function(r){return r.id;}).join(','));
|
||||
rec.sp = S._sp();
|
||||
rec.suspendedAt = Date.now();
|
||||
rec.waitKind = kind;
|
||||
|
|
@ -828,7 +823,7 @@
|
|||
S.installExportWraps([
|
||||
"wx_dom_event", "wx_dom_mouse", "wx_window_close", "wx_window_move",
|
||||
"wx_window_resize", "ProcessEvents", "wxWasmMailboxTick",
|
||||
"wxWasmTopLevelTick", "wxWasmMainLoopPump", "wxWasmJobTick"
|
||||
"wxWasmTopLevelTick", "wxWasmJobTick"
|
||||
]);
|
||||
// KiCad-only surfaces; both installers skip absent names, so the wx
|
||||
// test apps (no embind) pass through here untouched.
|
||||
|
|
|
|||
Loading…
Reference in a new issue