Phase E retry: K1 lands — early-resolve retention, not the lossy guard; pthread-ondemand link fixed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
This commit is contained in:
parent
54e6560037
commit
75209dc07e
6 changed files with 125 additions and 7 deletions
|
|
@ -1 +1 @@
|
||||||
12
|
13
|
||||||
|
|
|
||||||
|
|
@ -912,6 +912,80 @@ chooser, one lib request), not the full suite:
|
||||||
(`eeschema.spec.ts` or a chooser spec), not `npm run test:kicad` — this attempt cost a
|
(`eeschema.spec.ts` or a chooser spec), not `npm run test:kicad` — this attempt cost a
|
||||||
1.2-hour suite run to learn one bit. The full suite is the confirmation, not the probe.
|
1.2-hour suite run to learn one bit. The full suite is the confirmation, not the probe.
|
||||||
|
|
||||||
|
### Phase E retry: K1 LANDED — the strand was a certainty, not a race (2026-08-08)
|
||||||
|
|
||||||
|
**Gate: full KiCad suite 139 passed / 1 failed (pre-existing occ-probe glb) / 30
|
||||||
|
skipped in 7.2 min — the Phase A baseline exactly**, with the K1 conversion IN. The
|
||||||
|
casualty class went first: `eeschema-ui` 3/3 in 17.7 s (attempt 1 hung it at boot for
|
||||||
|
3 min per test), then strand + sim + modal-stack 6/6. Zero `[wx-wait]` beacons fired.
|
||||||
|
|
||||||
|
**The diagnosis, from evidence attempt 1 left behind rather than a rebuild.** The
|
||||||
|
broken run's playwright report survived (`tests/playwright-report`, 111 expected / 26
|
||||||
|
unexpected) and all 26 failures are the same shape: `#canvas` never appeared — BOOT
|
||||||
|
hangs, not chooser hangs. A live probe of the bare test page then supplied the missing
|
||||||
|
fact: **test pages install no `kicadLibs` provider at all.** So on every spec page the
|
||||||
|
converted bridge resolved its wait synchronously (no-hook → `resolveWait(token, 0)`
|
||||||
|
inside the same wasm turn), `resolveWait` DELETED the entry, and the C++ then parked a
|
||||||
|
context whose wake had already been spent. Not a race — a certainty on every
|
||||||
|
provider-less page, which is why 26 specs died broadly instead of one chooser flow.
|
||||||
|
(Hypothesis 1 was falsified first: both exports are real export assignments in the
|
||||||
|
surviving glue, and they come from the wx side of the link, identical in both builds.)
|
||||||
|
|
||||||
|
**The §"first attempt" guard is LOSSY and was NOT landed — this landed instead.** The
|
||||||
|
4-line `waits.has` guard returns 0 for an already-resolved wait, dropping the result
|
||||||
|
(a malloc'd payload pointer for lib requests, a return code for modals). The
|
||||||
|
result-preserving shape, all three pieces required together:
|
||||||
|
|
||||||
|
1. **Retention (shim):** `resolveWait` keeps a resolved-but-unawaited non-context
|
||||||
|
entry in the map with `result` attached (counter `earlyWaitResolves`), instead of
|
||||||
|
deleting it. Context-parked and promise-awaited resolves behave exactly as before.
|
||||||
|
2. **Peek-and-consume (wx):** `wxWasmYieldUntil` checks `waitEarlyResolved(token)`
|
||||||
|
before parking a context and returns `takeWaitResult(token)` instead of parking;
|
||||||
|
`waitPromise` consumes a retained entry too — which also fixes a PRE-EXISTING data
|
||||||
|
loss (a synchronous C++ resolve before `wxWasmYieldUntilJs` used to return 0 with
|
||||||
|
an "unknown token" warning).
|
||||||
|
3. **Deferred resolution (bridge contract):** a converted bridge must never call
|
||||||
|
`resolveWait` synchronously from its start function — every path defers to at
|
||||||
|
least a microtask. `pcbjam_libs_request_start` wraps the provider in
|
||||||
|
`Promise.resolve(...)` so even a synchronous provider resolves on a microtask.
|
||||||
|
|
||||||
|
Beacons on the two previously-silent links stay in: a resolve for an unregistered
|
||||||
|
context-wait token, and a refused `mark_ready`, both warn with the token identity.
|
||||||
|
|
||||||
|
**Why attempt 1 "reproduced with the guard in place" could not be reproduced:** not
|
||||||
|
resolvable from surviving evidence; the equivalent-but-stronger mechanism gates green
|
||||||
|
today. The stale-sync trap (a retest against unsynced binaries) remains the likeliest
|
||||||
|
explanation. The retention design is landed regardless — it is strictly stronger.
|
||||||
|
|
||||||
|
**Honest coverage note:** the bare-page probe shows `waitsBegun == 0` at boot — K1's
|
||||||
|
main-thread path is THIN on test pages (the chooser path proxies from pthread
|
||||||
|
workers, untouched). The park-then-resolve half of the mechanism is exercised by
|
||||||
|
every modal at D-off (same `wxWasmYieldUntil` path); the async-provider half needs
|
||||||
|
the standalone/web smoke before Phase E is called done. For K2–K7: the retention and
|
||||||
|
peek live in shared code — only the defer-the-resolve contract must be repeated per
|
||||||
|
bridge.
|
||||||
|
|
||||||
|
**A latent break the battery rerun un-hid (NOT this retry's doing):**
|
||||||
|
`coroutine-pthread-ondemand` aborted at `missing function:
|
||||||
|
_Z23pcbjam_context_sleep_msd` on both engines. The 8/7 context-sleep commit added a
|
||||||
|
bare `extern int pcbjam_context_sleep_ms(double)` to `wasm/shims/nanosleep_yield.c`;
|
||||||
|
the test-app Makefile compiles that file with `$(CXX)` and its comment ("em++ keys
|
||||||
|
language off the .c extension") is wrong — em++ compiles it as C++, so the
|
||||||
|
declaration MANGLES, and the standalone apps do not link `context_sleep.cpp` at all.
|
||||||
|
Broken since 8/7; unnoticed because the wx battery was never rerun after that commit
|
||||||
|
(the KiCad gate was). Fix in `nanosleep_yield.c`: the declaration is now
|
||||||
|
`extern "C"`-guarded AND `__attribute__((weak))` with a null check — an app without
|
||||||
|
the scheduler shim falls back to the in-place Asyncify yield, which is exactly the
|
||||||
|
behaviour those apps pin. Lesson for the phase log: **a wx-shim commit gated only on
|
||||||
|
the KiCad suite can silently break the standalone battery — run both nets when
|
||||||
|
`wasm/shims/` changes.**
|
||||||
|
|
||||||
|
**Battery after the fix: 388 passed / 8 failed / 3 skipped — the failure set is
|
||||||
|
EXACTLY the documented pre-existing D-off landing set** (nested case-3 ×3 tests ×2
|
||||||
|
engines, `wakeup_during_transition` layout-sensitive, `modal.spec.ts:125`), with
|
||||||
|
pthread-ondemand green on both engines. The shim's resolveWait retention change is
|
||||||
|
regression-free against every modal/nested/popup wait the battery stages.
|
||||||
|
|
||||||
1. **pthreads.** Doc 21 §2 settled that every Asyncify park is main-thread and the lib
|
1. **pthreads.** Doc 21 §2 settled that every Asyncify park is main-thread and the lib
|
||||||
bridge's worker path is a blocking proxy. Phase A must re-check that libcontext is never
|
bridge's worker path is a blocking proxy. Phase A must re-check that libcontext is never
|
||||||
driven from a worker before assuming the scheduler is main-thread-only.
|
driven from a worker before assuming the scheduler is main-thread-only.
|
||||||
|
|
|
||||||
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit c1ba4d3c3b086a4e0d9fdf04a4987dedef539662
|
Subproject commit ab1ac9b940596a97820eab3b8403f443db1e111a
|
||||||
|
|
@ -187,11 +187,12 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
// _wxNestedLoopExit — deleted at doc 20 D-1). Resolution flows
|
// _wxNestedLoopExit — deleted at doc 20 D-1). Resolution flows
|
||||||
// through the S2 deferred-wake law automatically: resolving a wait wakes
|
// through the S2 deferred-wake law automatically: resolving a wait wakes
|
||||||
// its parked sleep via the wrapped handleSleep path.
|
// its parked sleep via the wrapped handleSleep path.
|
||||||
waits: new Map(), // token → {kind, promise, resolve, resolved}
|
waits: new Map(), // token → {kind, promise, resolve, resolved, result, awaited, contextParked}
|
||||||
waitSeq: 0,
|
waitSeq: 0,
|
||||||
waitStacks: {}, // kind → [unresolved tokens], LIFO
|
waitStacks: {}, // kind → [unresolved tokens], LIFO
|
||||||
waitsBegun: 0,
|
waitsBegun: 0,
|
||||||
waitsResolved: 0,
|
waitsResolved: 0,
|
||||||
|
earlyWaitResolves: 0, // resolves that landed before their waiter parked (Phase E)
|
||||||
beginWait: function (kind) {
|
beginWait: function (kind) {
|
||||||
var token = ++this.waitSeq;
|
var token = ++this.waitSeq;
|
||||||
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
|
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
|
||||||
|
|
@ -208,8 +209,30 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
console.warn("[wx-scheduler] waitPromise(" + token + "): unknown token");
|
console.warn("[wx-scheduler] waitPromise(" + token + "): unknown token");
|
||||||
return Promise.resolve(0);
|
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;
|
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
|
// 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 —
|
// suspending its stack in place, so there is no promise anyone awaits —
|
||||||
// resolving one would strand the context forever. Marked from C++ at park
|
// resolving one would strand the context forever. Marked from C++ at park
|
||||||
|
|
@ -228,12 +251,12 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
var idx = stack.indexOf(token);
|
var idx = stack.indexOf(token);
|
||||||
if (idx !== -1) stack.splice(idx, 1);
|
if (idx !== -1) stack.splice(idx, 1);
|
||||||
}
|
}
|
||||||
this.waits.delete(token);
|
|
||||||
if (entry.contextParked) {
|
if (entry.contextParked) {
|
||||||
// Mark ready only — never resume inline. The pump picks it up from a
|
// 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
|
// 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
|
// contexts (a rewind inside this resolver's own turn is the whole
|
||||||
// class of bug the scheduler exists to remove).
|
// class of bug the scheduler exists to remove).
|
||||||
|
this.waits.delete(token);
|
||||||
try {
|
try {
|
||||||
Module["_wxWasmSchedResolveContextWait"](token, result | 0);
|
Module["_wxWasmSchedResolveContextWait"](token, result | 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -242,7 +265,18 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
this._armSchedPump();
|
this._armSchedPump();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
entry.result = result | 0;
|
||||||
entry.resolve(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;
|
return true;
|
||||||
},
|
},
|
||||||
// Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics).
|
// Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics).
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,18 @@ EM_ASYNC_JS( void, __wasm_main_thread_yield_ms, ( double ms ), {
|
||||||
* This is what makes TOOL_MANAGER::RunSynchronousAction's spin loop safe under
|
* This is what makes TOOL_MANAGER::RunSynchronousAction's spin loop safe under
|
||||||
* Phase D: an in-place park inside a tool body leaves a capture in flight for a
|
* Phase D: an in-place park inside a tool body leaves a capture in flight for a
|
||||||
* star transfer to land on (doRewind -> "index out of bounds").
|
* star transfer to land on (doRewind -> "index out of bounds").
|
||||||
|
*
|
||||||
|
* extern "C" + WEAK, both load-bearing: some app builds compile this file as
|
||||||
|
* C++ (em++ keys language off the DRIVER, not the .c extension — a bare extern
|
||||||
|
* here mangles and the app aborts at "missing function" on first sleep), and
|
||||||
|
* the standalone wx test apps do not link context_sleep.cpp at all. A weak
|
||||||
|
* null resolves to "no context lane here — always yield in place", which is
|
||||||
|
* exactly the pre-context behaviour those apps pin.
|
||||||
*/
|
*/
|
||||||
extern int pcbjam_context_sleep_ms( double ms );
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
#endif
|
||||||
|
int pcbjam_context_sleep_ms( double ms ) __attribute__(( weak ));
|
||||||
|
|
||||||
int nanosleep( const struct timespec* req, struct timespec* rem )
|
int nanosleep( const struct timespec* req, struct timespec* rem )
|
||||||
{
|
{
|
||||||
|
|
@ -52,7 +62,7 @@ int nanosleep( const struct timespec* req, struct timespec* rem )
|
||||||
double ms = (double) req->tv_sec * 1000.0 + (double) req->tv_nsec / 1.0e6;
|
double ms = (double) req->tv_sec * 1000.0 + (double) req->tv_nsec / 1.0e6;
|
||||||
if( emscripten_is_main_runtime_thread() )
|
if( emscripten_is_main_runtime_thread() )
|
||||||
{
|
{
|
||||||
if( !pcbjam_context_sleep_ms( ms ) )
|
if( !pcbjam_context_sleep_ms || !pcbjam_context_sleep_ms( ms ) )
|
||||||
__wasm_main_thread_yield_ms( ms ); /* yield -> event loop runs -> Worker boots */
|
__wasm_main_thread_yield_ms( ms ); /* yield -> event loop runs -> Worker boots */
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8f61561f061f1a38e98be5ac4b9c7b0fd2bbd234
|
Subproject commit 52608b79328a8aa577bdc587e8b474e6873060ee
|
||||||
Loading…
Reference in a new issue