design-b D-1b: delete the legacy handlesleep runtime from the injector
Doc 20 D-1, second slice: the injector now injects asyncify-scheduler.js unconditionally — the WX_SCHEDULER=0 legacy opt-out, the SHIM_DISABLE_HANDLESLEEP ablation skip, and the SHIM_DISABLE_TRAMPOLINE_HEAL ablation skip are gone, and scripts/common/shims/handlesleep.js is deleted (the scheduler subsumed its capture/restore, fiber guard, and trampoline-heal duties in S2). Comment-only scrubs point the remaining references at the scheduler shim. .ci-cache-epoch bumped (shim/injector behavior changed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
This commit is contained in:
parent
e7b8c2725b
commit
4906b9fb56
10 changed files with 34 additions and 477 deletions
|
|
@ -1 +1 @@
|
||||||
10
|
11
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ if [ "$make_rc" -ne 0 ]; then
|
||||||
exit "$make_rc"
|
exit "$make_rc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Inject the dyncall + handlesleep currData shims into every freshly-linked app. The
|
# Inject the dyncall + asyncify-scheduler shims into every freshly-linked app. The
|
||||||
# handlesleep currData save/restore (Emscripten #9153) is needed: without it a rewind that
|
# scheduler's currData save/restore (Emscripten #9153) is needed: without it a rewind that
|
||||||
# resumes through a fresh wasm re-entry hits _asyncify_start_rewind(null) -> "memory access out
|
# resumes through a fresh wasm re-entry hits _asyncify_start_rewind(null) -> "memory access out
|
||||||
# of bounds" — e.g. a context-menu pick while the main loop is parked. The Makefile only injects
|
# of bounds" — e.g. a context-menu pick while the main loop is parked. The Makefile only injects
|
||||||
# it for the coroutine apps; inject-dyncall-shims.sh is idempotent (skips an already-shimmed glue),
|
# it for the coroutine apps; inject-dyncall-shims.sh is idempotent (skips an already-shimmed glue),
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@
|
||||||
#
|
#
|
||||||
# The actual JavaScript that gets injected lives in readable, standalone files in
|
# The actual JavaScript that gets injected lives in readable, standalone files in
|
||||||
# scripts/common/shims/ (not inline heredocs):
|
# scripts/common/shims/ (not inline heredocs):
|
||||||
# - handlesleep.js nested-Asyncify handleSleep currData save/restore (#9153)
|
|
||||||
# - asyncify-scheduler.js the mailbox/scheduler (docs/features/async/17) —
|
# - asyncify-scheduler.js the mailbox/scheduler (docs/features/async/17) —
|
||||||
# the DEFAULT shim; WX_SCHEDULER=0 opts back into
|
# the ONLY asyncify runtime (the legacy handlesleep.js
|
||||||
# the legacy handlesleep.js
|
# opt-out was deleted at doc 20 D-1)
|
||||||
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
||||||
#
|
#
|
||||||
# Native wasm-EH is the only build mode, so the .js has no invoke_* wrappers / dynCall_<sig> call
|
# Native wasm-EH is the only build mode, so the .js has no invoke_* wrappers / dynCall_<sig> call
|
||||||
|
|
@ -20,7 +19,6 @@
|
||||||
# Usage:
|
# Usage:
|
||||||
# inject-dyncall-shims.sh <pcbnew.js>
|
# inject-dyncall-shims.sh <pcbnew.js>
|
||||||
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
||||||
# WX_SCHEDULER=0 inject-dyncall-shims.sh <pcbnew.js> # legacy opt-out (handlesleep.js instead of the scheduler)
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|
@ -36,7 +34,7 @@ if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
||||||
echo "Usage: $0 <path/to/pcbnew.js>"
|
echo "Usage: $0 <path/to/pcbnew.js>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for f in handlesleep.js asyncify-scheduler.js diagnostics.js; do
|
for f in asyncify-scheduler.js diagnostics.js; do
|
||||||
if [ ! -f "$SHIM_DIR/$f" ]; then
|
if [ ! -f "$SHIM_DIR/$f" ]; then
|
||||||
echo "Error: missing shim source $SHIM_DIR/$f"
|
echo "Error: missing shim source $SHIM_DIR/$f"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -73,25 +71,18 @@ apply_fix 'var iterFunc = (() => {});' 'var iterFunc = () => wasmExports["dynCal
|
||||||
apply_fix '(a1 => {})(userData);' 'wasmExports["dynCall_vi"](entryPoint, userData);' "fiber entry callback(s) (wasmExports.dynCall_vi)"
|
apply_fix '(a1 => {})(userData);' 'wasmExports["dynCall_vi"](entryPoint, userData);' "fiber entry callback(s) (wasmExports.dynCall_vi)"
|
||||||
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
|
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
|
||||||
|
|
||||||
# --- 3. Asyncify shim: legacy handleSleep fix OR the S2 scheduler --------------
|
# --- 3. Asyncify scheduler shim ------------------------------------------------
|
||||||
# Injected after Emscripten's fiber glue (the _emscripten_fiber_swap.isAsync marker),
|
# Injected after Emscripten's fiber glue (the _emscripten_fiber_swap.isAsync marker),
|
||||||
# or at EOF for non-fiber apps (a plain wx app still needs the currData machinery:
|
# or at EOF for non-fiber apps (a plain wx app still needs the currData machinery:
|
||||||
# without it a rewind resuming through a fresh wasm re-entry hits
|
# without it a rewind resuming through a fresh wasm re-entry hits
|
||||||
# _asyncify_start_rewind(null) -> "memory access out of bounds").
|
# _asyncify_start_rewind(null) -> "memory access out of bounds").
|
||||||
#
|
#
|
||||||
# WX_SCHEDULER (doc 17 S5): the scheduler is the DEFAULT since 2026-08-05 —
|
# asyncify-scheduler.js (docs/features/async/17) is the ONLY asyncify runtime:
|
||||||
# asyncify-scheduler.js REPLACES handlesleep.js. It subsumes the
|
# it owns the currData capture/restore, fiber guard, and trampoline heal, and
|
||||||
# capture/restore, fiber guard, and trampoline heal, and adds the deferred-wake
|
# adds the deferred-wake drain + N1 single-writer tripwire + the mailbox/wait
|
||||||
# drain + N1 single-writer tripwire + the mailbox/wait lanes. Injecting BOTH
|
# lanes. The legacy handlesleep.js opt-out (WX_SCHEDULER=0) and the ablation
|
||||||
# would double-manage the wake path (the scheduler refuses to install its core
|
# skip (SHIM_DISABLE_HANDLESLEEP) were deleted at doc 20 D-1 together with the
|
||||||
# then). WX_SCHEDULER=0 is the explicit legacy opt-out (kept until CI has run
|
# wx C++ paths they exercised.
|
||||||
# scheduler-only across the full matrix; the C++ legacy paths it exercises are
|
|
||||||
# deleted together with it — see doc 17 S5's demolition ledger).
|
|
||||||
#
|
|
||||||
# SHIM_DISABLE_HANDLESLEEP=1 skips the legacy shim: the asyncify-races red-green
|
|
||||||
# harness uses it to keep the historical "sleep buffer clobbered by fiber swap"
|
|
||||||
# crash reproducible. On WX_SCHEDULER=1 builds the variant still gets the
|
|
||||||
# scheduler — the ablation pins become scheduler-subsumption pins (doc 17 §3c).
|
|
||||||
inject_shim_at_marker() { # <shim file> <label>
|
inject_shim_at_marker() { # <shim file> <label>
|
||||||
local shim_file="$1" label="$2"
|
local shim_file="$1" label="$2"
|
||||||
local marker
|
local marker
|
||||||
|
|
@ -110,20 +101,12 @@ inject_shim_at_marker() { # <shim file> <label>
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "${WX_SCHEDULER:-1}" = "1" ]; then
|
# NOTE: idempotence via the shim-source sentinel, not __wxSchedulerInstalled —
|
||||||
# NOTE: idempotence via the shim-source sentinel, not __wxSchedulerInstalled —
|
# that string also appears in evtloop.cpp's EM_JS probe inside every glue.
|
||||||
# that string also appears in evtloop.cpp's EM_JS probe inside every glue.
|
if grep -q '__WX_SCHEDULER_SHIM_SOURCE__' "$JS_FILE"; then
|
||||||
if grep -q '__WX_SCHEDULER_SHIM_SOURCE__' "$JS_FILE"; then
|
echo "asyncify-scheduler already present - skipping"
|
||||||
echo "asyncify-scheduler already present - skipping"
|
|
||||||
else
|
|
||||||
inject_shim_at_marker asyncify-scheduler.js "asyncify-scheduler (default; replaces handlesleep)"
|
|
||||||
fi
|
|
||||||
elif [ "${SHIM_DISABLE_HANDLESLEEP:-0}" = "1" ]; then
|
|
||||||
echo "handleSleep fix DISABLED (SHIM_DISABLE_HANDLESLEEP=1) - ablation build"
|
|
||||||
elif grep -q '__nestedHandleSleepInstalled' "$JS_FILE"; then
|
|
||||||
echo "handleSleep fix already present - skipping"
|
|
||||||
else
|
else
|
||||||
inject_shim_at_marker handlesleep.js "handleSleep fix"
|
inject_shim_at_marker asyncify-scheduler.js "asyncify-scheduler"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
|
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
|
||||||
|
|
@ -168,12 +151,9 @@ fi
|
||||||
# so every later fiber swap silently fails to switch — the schematic load and all
|
# so every later fiber swap silently fails to switch — the schematic load and all
|
||||||
# post-idle tool actions hang. Wrap the loop in try/finally so the flag is always
|
# post-idle tool actions hang. Wrap the loop in try/finally so the flag is always
|
||||||
# reset (self-healing).
|
# reset (self-healing).
|
||||||
# SHIM_DISABLE_TRAMPOLINE_HEAL=1 skips it: used by the asyncify-races red-green
|
# (The SHIM_DISABLE_TRAMPOLINE_HEAL ablation skip was deleted at doc 20 D-1
|
||||||
# harness to keep the historical "park throw wedges the trampoline guard" hang
|
# with the races_test_noheal build that used it.)
|
||||||
# reproducible.
|
if grep -qF '} finally { Fibers.trampolineRunning = false; }' "$JS_FILE"; then
|
||||||
if [ "${SHIM_DISABLE_TRAMPOLINE_HEAL:-0}" = "1" ]; then
|
|
||||||
echo "fiber trampoline self-heal DISABLED (SHIM_DISABLE_TRAMPOLINE_HEAL=1) - ablation build"
|
|
||||||
elif grep -qF '} finally { Fibers.trampolineRunning = false; }' "$JS_FILE"; then
|
|
||||||
echo "fiber trampoline self-heal already present - skipping"
|
echo "fiber trampoline self-heal already present - skipping"
|
||||||
elif grep -qF 'Fibers.trampolineRunning = true;' "$JS_FILE"; then
|
elif grep -qF 'Fibers.trampolineRunning = true;' "$JS_FILE"; then
|
||||||
perl -0pi -e 's/(Fibers\.trampolineRunning = true;)(\s*)(do \{.*?\} while \(Fibers\.nextFiber\);)(\s*)(Fibers\.trampolineRunning = false;)/$1$2try {$3} finally { $5 }/s' "$JS_FILE"
|
perl -0pi -e 's/(Fibers\.trampolineRunning = true;)(\s*)(do \{.*?\} while \(Fibers\.nextFiber\);)(\s*)(Fibers\.trampolineRunning = false;)/$1$2try {$3} finally { $5 }/s' "$JS_FILE"
|
||||||
|
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
// === Nested-Asyncify handleSleep currData save/restore (Emscripten #9153) ===
|
|
||||||
//
|
|
||||||
// Asyncify.currData is a single-slot global. When a fiber swap runs inside an
|
|
||||||
// EM_ASYNC_JS Promise await (e.g., wxDialog::ShowModal via startModal), the
|
|
||||||
// fiber swap overwrites currData with the fiber's asyncify_data, losing the
|
|
||||||
// sleep's own buffer. On Promise resolution, handleSleep's doRewind then uses
|
|
||||||
// the wrong buffer and crashes with "index out of bounds" or "unreachable".
|
|
||||||
//
|
|
||||||
// Workaround: intercept Asyncify.allocateData to record which pointer belongs to
|
|
||||||
// the active handleSleep; restore it to Asyncify.currData inside the wakeUp
|
|
||||||
// callback before handleSleep proceeds to _asyncify_start_rewind + doRewind.
|
|
||||||
if (typeof Asyncify !== "undefined") {
|
|
||||||
if (typeof Asyncify.handleSleep === "function"
|
|
||||||
&& typeof Asyncify.allocateData === "function"
|
|
||||||
&& !Asyncify.__nestedHandleSleepInstalled) {
|
|
||||||
// Stack of handleSleep contexts awaiting their allocateData association.
|
|
||||||
Asyncify.__pendingSleepContexts = [];
|
|
||||||
|
|
||||||
// Anomaly reporting (diagnostics only — behavior unchanged). This shim has
|
|
||||||
// been SILENTLY repairing currData aliasing between concurrent parks since
|
|
||||||
// it was written; production traps in exactly this family ("index out of
|
|
||||||
// bounds" / "unreachable executed" during doRewind) keep arriving with no
|
|
||||||
// way to tell whether the shim fired, mislinked, or was bypassed (fiber
|
|
||||||
// swaps don't allocate through allocateData). Make every repair and every
|
|
||||||
// concurrent-park window loud, so a saved console dump answers that.
|
|
||||||
// Rate-limited per kind: first 10 in full, then every 100th.
|
|
||||||
// Flight recorder: a capped ring of asyncify/fiber events (never printed
|
|
||||||
// during normal operation), dumped to the console ONCE when a trap
|
|
||||||
// signature surfaces — so a prod console export carries the exact event
|
|
||||||
// sequence and machine state at death instead of just stack shapes.
|
|
||||||
// window.__wxAsyncifyDump() returns it on demand.
|
|
||||||
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)");
|
|
||||||
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify.__rec.join("\n ");
|
|
||||||
};
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.__wxAsyncifyDump = __dumpState;
|
|
||||||
// Auto-dump beside the first trap signatures in the console — the one
|
|
||||||
// artifact prod reports reliably contain.
|
|
||||||
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) {
|
|
||||||
// The stack names WHICH EM_ASYNC_JS parked (__asyncjs__wxWasmYieldToBrowser,
|
|
||||||
// startModal, js_enumerateFonts, ...) — the missing actor in every prod dump.
|
|
||||||
try { line += "\n" + String(new Error().stack).split("\n").slice(1, 8).join("\n"); } catch (e) {}
|
|
||||||
}
|
|
||||||
console.warn(line);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
|
|
||||||
Asyncify.allocateData = function() {
|
|
||||||
var ptr = __originalAllocateData();
|
|
||||||
// Associate with the innermost pending handleSleep not yet linked.
|
|
||||||
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) {
|
|
||||||
// A FRESH park (state 0 = Normal) starting while another chain's park is
|
|
||||||
// still live: the single-slot currData is about to be overwritten. The
|
|
||||||
// shim's restore below makes the POINTER survive, but nothing protects
|
|
||||||
// deeper state (fiber swaps, freed buffers, out-of-order wakes) — this
|
|
||||||
// window is where the trap family lives, and until now it was invisible.
|
|
||||||
// state 2 (Rewinding) entries are NOT reported: every resume legally
|
|
||||||
// re-enters handleSleep while rewinding with currData set (verified
|
|
||||||
// empirically 2026-07-31 — the timer-park e2e produced ~100/s of them
|
|
||||||
// on a healthy run).
|
|
||||||
__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) {
|
|
||||||
// Parking while an UNWIND is literally in progress is never legal —
|
|
||||||
// if this ever fires it IS the bug.
|
|
||||||
__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 — pushing a context for it leaks one per resume (the v0.1.23
|
|
||||||
// prod dump carried ~380 zero-linked pending contexts).
|
|
||||||
if (Asyncify.state !== 0) {
|
|
||||||
return __originalHandleSleep(startAsync);
|
|
||||||
}
|
|
||||||
var sleepCtx = {
|
|
||||||
capturedData: null,
|
|
||||||
cleanedUp: false,
|
|
||||||
// Ownership: does this park belong to the ROOT chain (the main
|
|
||||||
// loop's yield, an embind entry) or to a fiber body's slice? Root
|
|
||||||
// re-entry during a ROOT-owned wake is the nested-self-rewind that
|
|
||||||
// kills prod; fiber-owned wakes completing into root are the benign
|
|
||||||
// bulk (see the fiber guard below).
|
|
||||||
rootOwned: (typeof Fibers === "undefined")
|
|
||||||
|| (!Fibers.__inFiberEntry
|
|
||||||
&& !(Asyncify.__wakingOwnerFiber || false))
|
|
||||||
};
|
|
||||||
Asyncify.__pendingSleepContexts.push(sleepCtx);
|
|
||||||
|
|
||||||
var cleanup = function() {
|
|
||||||
if (sleepCtx.cleanedUp) return;
|
|
||||||
sleepCtx.cleanedUp = true;
|
|
||||||
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
|
|
||||||
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
return __originalHandleSleep(function(wakeUp) {
|
|
||||||
return startAsync(function(result) {
|
|
||||||
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
|
|
||||||
// the await may have overwritten Asyncify.currData. Restore OUR buffer
|
|
||||||
// so handleSleep's _asyncify_start_rewind and doRewind use it.
|
|
||||||
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0)
|
|
||||||
+ (sleepCtx.rootOwned ? " R" : " f"));
|
|
||||||
if (sleepCtx.capturedData) {
|
|
||||||
if (Asyncify.currData !== sleepCtx.capturedData) {
|
|
||||||
// The repair firing. currData=null → the overlapping chain
|
|
||||||
// already completed (benign overlap, but COUNT it: it proves
|
|
||||||
// concurrent parks happen on this load). currData=<other> → a
|
|
||||||
// DIFFERENT chain is parked right now and we are rewinding
|
|
||||||
// around it — the dangerous interleave.
|
|
||||||
__wxAsyncifyReport(
|
|
||||||
Asyncify.currData ? "aliased-wake-live" : "overlapped-wake",
|
|
||||||
"restoring currData=" + sleepCtx.capturedData +
|
|
||||||
" over " + (Asyncify.currData || "null") +
|
|
||||||
" state=" + Asyncify.state,
|
|
||||||
!!Asyncify.currData);
|
|
||||||
}
|
|
||||||
Asyncify.currData = sleepCtx.capturedData;
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
// Mark the synchronous wake window: everything below wakeUp() —
|
|
||||||
// the rewind, the resumed code running forward, its next unwind —
|
|
||||||
// executes inside it. A fiber completion whose root-entry lands
|
|
||||||
// in this window rewinds the root WHILE the wake's own rewind is
|
|
||||||
// in flight (the four identical prod trap stacks:
|
|
||||||
// maybeStopUnwind → trampoline → finishContextSwitch →
|
|
||||||
// doRewind(root) → unreachable). The stale-fiber guard below
|
|
||||||
// defers such root entries by one macrotask.
|
|
||||||
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) {
|
|
||||||
// emscripten_set_main_loop(...,1) parks main() by throwing the
|
|
||||||
// "unwind" sentinel. When main's LAST pre-park suspension was a
|
|
||||||
// sleep, main is resumed from THIS wakeUp, so the sentinel
|
|
||||||
// propagates here instead of into callMain's catch — surfacing as
|
|
||||||
// an uncaught "unwind" promise rejection. Swallow it exactly like
|
|
||||||
// callMain/handleException do on the direct path.
|
|
||||||
if (e === "unwind") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
Asyncify.__inSleepWake -= 1;
|
|
||||||
Asyncify.__wakingOwnerFiber = prevWakingOwnerFiber;
|
|
||||||
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = prevWakingRoot;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Asyncify.__nestedHandleSleepInstalled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// === End nested-Asyncify handleSleep fix ===
|
|
||||||
|
|
||||||
// === Stale-fiber-rewind guard (the decoded 2026-07/08 prod board-load trap) ===
|
|
||||||
//
|
|
||||||
// A fiber whose body asyncify-parks inside handleSleep is suspended in a way
|
|
||||||
// the fiber machinery cannot see: its struct still holds the CONSUMED data of
|
|
||||||
// its last real swap-out. The C++ libcontext guard (swap_suspended) closes the
|
|
||||||
// simple case, but caller attribution can be poisoned — a fresh JS entry that
|
|
||||||
// jumps while g_current_context still points at a parked fiber writes a fresh
|
|
||||||
// suspension INTO that parked fiber's struct, so the flag lies. This guard is
|
|
||||||
// attribution-proof: it tracks validity at the emscripten-fiber layer itself.
|
|
||||||
//
|
|
||||||
// A fiber becomes safely resumable ONLY when a real swap-out writes its
|
|
||||||
// suspension — observable here because fiber_swap sets Asyncify.currData to
|
|
||||||
// oldFiber's asyncify data (fiber+20) and finishContextSwitch runs before
|
|
||||||
// anything else touches it. Consuming a suspension (the rewind path) removes
|
|
||||||
// it. A suspended-path entry for a fiber with NO live suspension is exactly
|
|
||||||
// the stale rewind that produced "unreachable executed" + a poisoned runtime
|
|
||||||
// (docs/features/async/16) — REFUSE it: the dropped dispatch ghost-resolves
|
|
||||||
// (the jump-ghost contract), the parked body completes via its own wake.
|
|
||||||
if (typeof Fibers !== "undefined"
|
|
||||||
&& typeof Fibers.finishContextSwitch === "function"
|
|
||||||
&& !Fibers.__staleRewindGuardInstalled) {
|
|
||||||
// Fibers whose last swap-out wrote a live (unconsumed) suspension.
|
|
||||||
Fibers.__validSuspensions = new Set();
|
|
||||||
// Fibers whose last slice ended in a handleSleep park instead of a swap-out:
|
|
||||||
// their body is mid-sleep, so entering them is unsafe no matter what their
|
|
||||||
// struct holds (a misattributed jump may have written a valid-LOOKING
|
|
||||||
// foreign suspension into it).
|
|
||||||
Fibers.__internallyParked = new Set();
|
|
||||||
// fiber → the sleep buffer its internal park is waiting on. A LATER
|
|
||||||
// "swap-out" of that fiber is genuine only if this sleep has resolved
|
|
||||||
// (its context left __pendingSleepContexts) — a misattributed jump from a
|
|
||||||
// fresh JS entry writes the fiber's struct while the sleep is still
|
|
||||||
// pending, and must not launder the fiber back into the valid set.
|
|
||||||
Fibers.__parkSleepBuf = new Map();
|
|
||||||
|
|
||||||
var __origFinishContextSwitch = Fibers.finishContextSwitch.bind(Fibers);
|
|
||||||
var __fiberRefusals = 0;
|
|
||||||
|
|
||||||
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 + ")");
|
|
||||||
}
|
|
||||||
// No context is entered. The unwind that got us here already completed
|
|
||||||
// (state Normal); clear the dangling currData so the next fresh park
|
|
||||||
// does not read a foreign pointer.
|
|
||||||
Asyncify.currData = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
var __fcsRec = (typeof Asyncify !== "undefined" && Asyncify.__recPush)
|
|
||||||
? Asyncify.__recPush
|
|
||||||
: function() {};
|
|
||||||
|
|
||||||
Fibers.finishContextSwitch = function(newFiber) {
|
|
||||||
// Cumulative, scroll-proof counters (the 96-event ring holds <1s at idle
|
|
||||||
// tick rate — differential-repro dose measurements need totals).
|
|
||||||
Fibers.__fcsTotal = (Fibers.__fcsTotal || 0) + 1;
|
|
||||||
if (newFiber === Fibers.__rootFiber && (Asyncify.__inSleepWake || 0) > 0) {
|
|
||||||
Fibers.__rootHotTotal = (Fibers.__rootHotTotal || 0) + 1;
|
|
||||||
}
|
|
||||||
// rem = free bytes left in the old side's asyncify buffer after its unwind
|
|
||||||
// finished (asyncify_data layout: [data]=write ptr, [data+4]=buffer end).
|
|
||||||
// A deep capture that exhausts the 512K fiber buffer overflows SILENTLY in
|
|
||||||
// release — rem at/below 0 here is the smoking gun for an unrewindable
|
|
||||||
// suspension (the 2026-08-03 deferred-retry trap hypothesis).
|
|
||||||
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])
|
|
||||||
// The rewind entry recorded for this suspension (setDataRewindFunc
|
|
||||||
// wrote exportCallStack[0] at unwind start) vs the export stack NOW —
|
|
||||||
// a capture taken under a NESTED export whose recorded entry is the
|
|
||||||
// outer _main-style bottom is unrewindable (the 2026-08-03 trap).
|
|
||||||
+ " 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);
|
|
||||||
// The swap that scheduled this switch just suspended its old fiber and
|
|
||||||
// left currData = oldFiber+20 (fiber_swap's unwind path); record that
|
|
||||||
// suspension as live — and a GENUINE swap-out also ends any internal
|
|
||||||
// park. Genuine means the fiber's pending sleep (if any) has resolved;
|
|
||||||
// otherwise this is a misattributed fresh-entry jump writing into a
|
|
||||||
// parked fiber's struct, and the fiber must stay quarantined.
|
|
||||||
// finishContextSwitch only runs for genuine fiber switches, so currData
|
|
||||||
// here is never a handleSleep buffer.
|
|
||||||
if (Asyncify.currData) {
|
|
||||||
var oldFiber = Asyncify.currData - 20;
|
|
||||||
// The very first switch is always main → coroutine: remember the ROOT
|
|
||||||
// context. The root is exempt from quarantine below — after a rewind
|
|
||||||
// into it, execution continues into the whole main loop (which parks in
|
|
||||||
// its yield as a matter of course); reading that park as "the entered
|
|
||||||
// fiber is mid-body" quarantined MAIN and starved every coroutine
|
|
||||||
// return (empty collab results across the board on the first build of
|
|
||||||
// this guard).
|
|
||||||
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;
|
|
||||||
|
|
||||||
// DEFERRAL FAMILY CLOSED (2026-08-03, round 6 — the definitive finding).
|
|
||||||
// Deferral at this layer can never work: by the time finishContextSwitch
|
|
||||||
// runs, _asyncify_start_unwind has ALREADY written the suspension, and a
|
|
||||||
// root suspension captured inside main's own live wake window is broken
|
|
||||||
// AT WRITE TIME — emscripten_fiber_swap records the rewind entry as
|
|
||||||
// exportCallStack[0] (the re-invoked main export) while the capture only
|
|
||||||
// spans the swap-site frames (1.3KB vs a valid fresh-entry's ~10KB). A
|
|
||||||
// microtask-deferred retry on a clean empty stack trapped IDENTICALLY to
|
|
||||||
// the nested rewind. Prevention lives where it must: the libcontext C++
|
|
||||||
// guard refuses the jump BEFORE the unwind starts (jump-refused-hot-main,
|
|
||||||
// keyed on __wakingRoot via EM_JS) — same ghost contract as the parked
|
|
||||||
// guard. The consume-once/quarantine checks below remain the backstop
|
|
||||||
// for laundered attributions the C++ layer cannot see.
|
|
||||||
|
|
||||||
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
|
||||||
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
|
||||||
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
|
|
||||||
// Root is exempt from THIS check only: it "parks" in the main loop's
|
|
||||||
// yield as a matter of course (quarantining it starved every coroutine
|
|
||||||
// return — 19 collab e2e reds on the first guard build).
|
|
||||||
__refuseFiber(newFiber, "is asyncify-parked mid-body (sleep in flight)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (entryPoint === 0) {
|
|
||||||
// Suspended-fiber path: about to rewind newFiber+20 — root INCLUDED.
|
|
||||||
// Consume-once semantics are the actual prod killer's cure (all four
|
|
||||||
// trap stacks, v0.1.19–22): each fiber_swap suspension is rewindable
|
|
||||||
// exactly once. Two fibers completing against ONE root suspension
|
|
||||||
// epoch (a tool fiber and a collab fiber both waking around
|
|
||||||
// open:settled) makes the second finishContextSwitch(root) rewind
|
|
||||||
// already-consumed data → "unreachable executed" → poisoned runtime.
|
|
||||||
// Refusing the second consumption loses nothing: the fiber that
|
|
||||||
// yielded stays properly suspended (recorded above), and the root
|
|
||||||
// continues via its real pending resume (its own sleep wake or the
|
|
||||||
// next fresh JS entry) — the same contract as libcontext's
|
|
||||||
// ghost-resume epochs, enforced one layer lower.
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleeps started inside an entered FIBER's slice are fiber-owned (see the
|
|
||||||
// handleSleep wrapper's rootOwned tag). Root entries don't count: the
|
|
||||||
// main loop's continuation after a root rewind is root-owned by
|
|
||||||
// definition — that's exactly the chain whose wake must not be re-entered.
|
|
||||||
if (!isRoot) Fibers.__inFiberEntry = (Fibers.__inFiberEntry || 0) + 1;
|
|
||||||
var ret;
|
|
||||||
try {
|
|
||||||
ret = __origFinishContextSwitch(newFiber);
|
|
||||||
} finally {
|
|
||||||
if (!isRoot) Fibers.__inFiberEntry -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// How did the entered fiber's synchronous slice end? Another fiber swap
|
|
||||||
// (nextFiber set — the trampoline loop continues, proper suspension) or a
|
|
||||||
// handleSleep park (currData holds a sleep buffer — the body is mid-sleep
|
|
||||||
// and must not be entered until it properly swaps out). Never applied to
|
|
||||||
// the root: its rewound continuation runs the whole main loop, whose
|
|
||||||
// routine yield park says nothing about a fiber body.
|
|
||||||
if (!isRoot && !Fibers.nextFiber && Asyncify.currData) {
|
|
||||||
Fibers.__internallyParked.add(newFiber);
|
|
||||||
Fibers.__parkSleepBuf.set(newFiber, Asyncify.currData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
Fibers.__staleRewindGuardInstalled = true;
|
|
||||||
}
|
|
||||||
// === End stale-fiber-rewind guard ===
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { test, expect, tryLoadApp } from '../e2e/utils/fixtures';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* S2 scheduler-core gates (docs/features/async/17 §3d N1/N4, §4 S2).
|
* S2 scheduler-core gates (docs/features/async/17 §3d N1/N4, §4 S2).
|
||||||
* Runs against the races harness built with WX_SCHEDULER=1 (scheduler-only
|
* Runs against the races harness; the scheduler shim is the only runtime
|
||||||
* glue — the legacy handlesleep shim is NOT injected on scheduler builds).
|
* (the legacy handlesleep opt-out was deleted at doc 20 D-1). The self-skip
|
||||||
* Both tests self-skip on legacy glue, so the file is safe in either variant.
|
* on shim-less glue is kept as a stale-build guard.
|
||||||
*
|
*
|
||||||
* N1 — single-writer tripwire: Asyncify.currData is an accessor; a pure-JS
|
* N1 — single-writer tripwire: Asyncify.currData is an accessor; a pure-JS
|
||||||
* write without scheduler authorization beacons (and throws in strict mode).
|
* write without scheduler authorization beacons (and throws in strict mode).
|
||||||
|
|
@ -48,7 +48,7 @@ async function bootAndSettle(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('S2 scheduler core (WX_SCHEDULER=1 glue)', () => {
|
test.describe('S2 scheduler core (scheduler glue)', () => {
|
||||||
test('N4: battery leaves coherent books — wakes drained, no strays, battery green', async ({
|
test('N4: battery leaves coherent books — wakes drained, no strays, battery green', async ({
|
||||||
page,
|
page,
|
||||||
testLogger,
|
testLogger,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import { test, expect } from "./fixtures";
|
||||||
* - mid-load applies are DROPPED (the probe segment must not move);
|
* - mid-load applies are DROPPED (the probe segment must not move);
|
||||||
* - after settle the entries work normally (guard released).
|
* - after settle the entries work normally (guard released).
|
||||||
*
|
*
|
||||||
* VARIANT CONTRACT (docs/features/async/17 §3b): on WX_SCHEDULER=1 glue the
|
* VARIANT CONTRACT (docs/features/async/17 §3b): on scheduler glue the
|
||||||
* shim's embind lane queues busy-window mutators and delivers them after
|
* shim's embind lane queues busy-window mutators and delivers them after
|
||||||
* settle, so the "applies are DROPPED" assertions flip to "applies are
|
* settle, so the "applies are DROPPED" assertions flip to "applies are
|
||||||
* DELIVERED in order" — assertSettledContract branches on the lane's
|
* DELIVERED in order" — assertSettledContract branches on the lane's
|
||||||
|
|
@ -301,7 +301,7 @@ async function assertSettledContract(page: Page, stats: FuzzStats): Promise<void
|
||||||
|
|
||||||
// Variant contract (docs/features/async/17 §3b). Legacy glue: the gate
|
// Variant contract (docs/features/async/17 §3b). Legacy glue: the gate
|
||||||
// DROPPED the mid-load applies — the probe never moved. Scheduler glue
|
// DROPPED the mid-load applies — the probe never moved. Scheduler glue
|
||||||
// (WX_SCHEDULER=1 shim embind lane, doc 18): the same applies were QUEUED
|
// (scheduler shim embind lane, doc 18): the same applies were QUEUED
|
||||||
// and DELIVERED after settle, in order — the probe sits where the hammer's
|
// and DELIVERED after settle, in order — the probe sits where the hammer's
|
||||||
// deltas moved it. Same stimulus, the drop→deliver flip is the assertion.
|
// deltas moved it. Same stimulus, the drop→deliver flip is the assertion.
|
||||||
const schedulerLane = await page.evaluate(
|
const schedulerLane = await page.evaluate(
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ function runLoadPcbTest(demo: DemoCfg): void {
|
||||||
// the load — not before, not after the board renders. The formerly
|
// the load — not before, not after the board renders. The formerly
|
||||||
// tolerated post-load clipboard/unwind RuntimeErrors are fixed
|
// tolerated post-load clipboard/unwind RuntimeErrors are fixed
|
||||||
// (sync clipboard IsSupported in wx; "unwind" sentinel handling in
|
// (sync clipboard IsSupported in wx; "unwind" sentinel handling in
|
||||||
// scripts/common/shims/handlesleep.js; see docs/features/asyncify-arbiter/).
|
// the scheduler shim; see docs/features/asyncify-arbiter/).
|
||||||
const asyncifySignatures = [
|
const asyncifySignatures = [
|
||||||
'index out of bounds',
|
'index out of bounds',
|
||||||
'indirect call to null',
|
'indirect call to null',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { test, expect } from "./fixtures";
|
||||||
*
|
*
|
||||||
* The mailbox flips drop→deliver: a mutating entry issued during the open
|
* The mailbox flips drop→deliver: a mutating entry issued during the open
|
||||||
* becomes a queued message, applied IN ORDER after the open completes. GREEN
|
* becomes a queued message, applied IN ORDER after the open completes. GREEN
|
||||||
* since S1's embind lane — the WX_SCHEDULER=1 shim wraps the audited mutators
|
* since S1's embind lane — the scheduler shim wraps the audited mutators
|
||||||
* (doc 18) at the Module boundary, queueing busy-window calls and delivering
|
* (doc 18) at the Module boundary, queueing busy-window calls and delivering
|
||||||
* after settle with promise-returned results. S4 moves queueing worker-side.
|
* after settle with promise-returned results. S4 moves queueing worker-side.
|
||||||
* Self-skips on legacy glue (the lane is a build variant until S5).
|
* Self-skips on legacy glue (the lane is a build variant until S5).
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import { expectGuardsSilent } from "./utils/guard-beacons";
|
||||||
* and the main loop spends most wall-clock time Asyncify-parked inside
|
* and the main loop spends most wall-clock time Asyncify-parked inside
|
||||||
* wxWasmYieldToBrowser. A timer handler that itself parks therefore creates
|
* wxWasmYieldToBrowser. A timer handler that itself parks therefore creates
|
||||||
* TWO live Asyncify contexts over the single-slot `Asyncify.currData` — the
|
* TWO live Asyncify contexts over the single-slot `Asyncify.currData` — the
|
||||||
* emscripten #9153 family that scripts/common/shims/handlesleep.js silently
|
* emscripten #9153 family that the scheduler shim
|
||||||
* repairs. The collab entries add the third ingredient: they run on
|
* (scripts/common/shims/asyncify-scheduler.js) silently repairs. The collab entries add the third ingredient: they run on
|
||||||
* TOOL_MANAGER coroutines (emscripten_fiber_swap), which bypass the shim's
|
* TOOL_MANAGER coroutines (emscripten_fiber_swap), which bypass the shim's
|
||||||
* allocateData accounting entirely — and `finishContextSwitch` is exactly
|
* allocateData accounting entirely — and `finishContextSwitch` is exactly
|
||||||
* where the prod trap's second stack dies.
|
* where the prod trap's second stack dies.
|
||||||
|
|
@ -328,7 +328,7 @@ test.describe("timer Notify() Asyncify-park during main-loop yield (concurrent c
|
||||||
);
|
);
|
||||||
if (cLane) expectGuardsSilent(testLogger.consoleLogs, ["timerRetry"]);
|
if (cLane) expectGuardsSilent(testLogger.consoleLogs, ["timerRetry"]);
|
||||||
|
|
||||||
// Window-engagement proof, independent of survival: the handlesleep shim
|
// Window-engagement proof, independent of survival: the scheduler shim
|
||||||
// must have SEEN the concurrent parks (its reporting is new — silence here
|
// must have SEEN the concurrent parks (its reporting is new — silence here
|
||||||
// means the lever never created the overlap and the repro is vacuous).
|
// means the lever never created the overlap and the repro is vacuous).
|
||||||
const shimLines = testLogger.consoleLogs.filter((l) => l.includes("[wx-asyncify]"));
|
const shimLines = testLogger.consoleLogs.filter((l) => l.includes("[wx-asyncify]"));
|
||||||
|
|
@ -336,7 +336,7 @@ test.describe("timer Notify() Asyncify-park during main-loop yield (concurrent c
|
||||||
for (const l of shimLines.slice(0, 10)) console.log(`[TEST] ${l}`);
|
for (const l of shimLines.slice(0, 10)) console.log(`[TEST] ${l}`);
|
||||||
expect(
|
expect(
|
||||||
shimLines.length,
|
shimLines.length,
|
||||||
"handlesleep shim observed the concurrent-park window",
|
"scheduler shim observed the concurrent-park window",
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export interface GuardBeaconCounts {
|
||||||
timerRetry: BeaconFamilyCount;
|
timerRetry: BeaconFamilyCount;
|
||||||
// wx dispatch interlock bookkeeping anomalies (evtloop.cpp)
|
// wx dispatch interlock bookkeeping anomalies (evtloop.cpp)
|
||||||
dispatchAnomaly: BeaconFamilyCount;
|
dispatchAnomaly: BeaconFamilyCount;
|
||||||
// handlesleep.js shim: nested-park / wake-aliasing / stale-fiber refusals
|
// asyncify-scheduler.js shim: nested-park / wake-aliasing / stale-fiber refusals
|
||||||
wxAsyncify: BeaconFamilyCount;
|
wxAsyncify: BeaconFamilyCount;
|
||||||
// libcontext swap-layer refusals + hot-main beacons ([collab-fcontext])
|
// libcontext swap-layer refusals + hot-main beacons ([collab-fcontext])
|
||||||
libcontext: BeaconFamilyCount;
|
libcontext: BeaconFamilyCount;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue