docs 22: Phase E first attempt (K1 as a context wait) - REVERTED, with the post-mortem

Converting the symbol-library bridge from one EM_ASYNC_JS into
beginWait -> start -> wxWasmYieldUntil -> take_result made the app hang after
every library request: the KiCad suite went from 7 minutes to 1.2 HOURS, 111
passed with the rest timing out, and the log goes silent right after a lib
request - a parked context nobody resumes. Reverted; the tree keeps the
working EM_ASYNC_JS and no code from the attempt.

Recorded for the next attempt:

- The PRECONDITION it found (four lines, quoted in the doc, deliberately NOT
  landed): wxWasmYieldUntil must not park a context whose wait is already
  resolved, because resolveWait deletes the entry. The in-place form had no
  such window; every bridge in this pattern re-opens it. It was not the hang,
  but it has only ever existed in the broken build, so it lands WITH the
  conversion it protects, not before.
- Three hypotheses in evidence order, cheapest first: the resolve never
  reaching the registry (this is the first KiCad-side caller of the wait
  registry - check Module["_wxWasmSchedResolveContextWait"] exists in the
  KiCad link); the lib request running inside the chooser's modal where the
  dispatch context is ALREADY parked on a nested wait (a context holds one
  park); or the Asyncify instrumentation closure shifting when the
  EM_ASYNC_JS left that translation unit.
- Process: gate a bridge conversion on a SINGLE spec, never the full suite.
  This cost a 1.2-hour run to learn one bit.

Tree is unchanged from the last verified landing state (STAR_DISPATCH=0,
kicad 139 passed / 1 pre-existing occ-probe) - docs only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
This commit is contained in:
Gergő Törcsvári 2026-08-08 12:58:40 +02:00
commit 54e6560037
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322

View file

@ -848,6 +848,70 @@ everything kept. **Next: Phase E** — the K1K7 bridges are now the only in-p
left under a context, and they are what the `current() != 0` fallback above still
tolerates.
### Phase E, first attempt: K1 as a context wait — REVERTED, and what it cost
The bridge pattern doc 22 §5 calls for, tried on K1 (`sch_io_pcbjam_lib.cpp`, the
symbol-library request — chosen first because doc 21 makes it the doc-19 exposure: a
chooser's lazy load parks the TOOL COROUTINE that owns the chooser):
```
before: char* r = pcbjam_libs_request_js(op, lib, arg, kind); // EM_ASYNC_JS, parks in place
after: int t = wxWasmBeginWait("lib");
pcbjam_libs_request_start(t, op, lib, arg, kind); // EM_JS, resolves the wait
wxWasmYieldUntil(t); // context park, or in-place fallback
char* r = pcbjam_libs_take_result(t);
```
**Result: the KiCad suite went from 7 minutes to 1.2 HOURS — 111 passed, the rest
timing out, and the app going SILENT right after a library request.** A parked context
that nobody resumes. Reverted; the tree keeps the working `EM_ASYNC_JS`.
**A PRECONDITION the attempt discovered — re-add it WITH the next conversion, it is
not in the tree.** `wxWasmYieldUntil` must not park a context whose wait is already
resolved: `resolveWait` DELETES the entry, so a bridge whose promise settles before the
caller reaches the park leaves that context waiting for a wake nobody sends. The
in-place form had no such window (the same expression created and awaited the promise);
every bridge converted to this pattern re-opens it. The guard is four lines —
```cpp
EM_JS(int, wxWasmWaitPendingJs, (int token), { // resolveWait deletes the entry
return globalThis.__wxScheduler.waits.has(token) ? 1 : 0;
});
...
if (self && pcbjam_sched::can_yield_here())
{
if (!wxWasmWaitPendingJs(token)) // already resolved: do NOT park
return 0;
```
— and it was NOT the hang (the hang reproduced with it in place). It is deliberately
NOT landed: it sits in the wait path of every modal and nested loop, and the only build
that ever contained it was the broken one, so it has never been measured at D-off. It
belongs in the same commit as the bridge conversion it protects, gated together.
**Where the next attempt should start — hypotheses, in the order the evidence supports
them.** Not yet distinguished; do this with a focused repro (open a schematic with the
chooser, one lib request), not the full suite:
1. **The resolve never reaches the registry.** `resolveWait` routes a context-parked
token to `Module["_wxWasmSchedResolveContextWait"]` + `_armSchedPump`; if either
export is missing from the KiCad link (they are `EMSCRIPTEN_KEEPALIVE` in wx, but
this is the first KiCad-side caller of the wait registry) the mark-ready silently
never happens. Check `Module["_wxWasmSchedResolveContextWait"]` in the console
first — it is one line and would explain the symptom exactly.
2. **The lib path runs where `can_yield_here()` is true but the resumer cannot reach
it** — e.g. inside the chooser's modal, where the dispatch context is already
parked at a "nested" wait and the lib wait parks it a second time (a context can
hold only ONE park; the second yield would strand the first).
3. **Asyncify instrumentation closure.** Removing the `EM_ASYNC_JS` from this
translation unit changes which functions binaryen instruments; the KiCad lib path
now reaches an unwind only through `wxWasmYieldUntilJs`. If the closure no longer
covers a frame in that path, the fallback park corrupts instead of suspending.
**Process note for the next attempt:** gate a bridge conversion on a SINGLE spec first
(`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. **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
driven from a worker before assuming the scheduler is main-thread-only.