pcbjam/docs/features/async/19-quasimodal-fiber-strand.md
Gergő Törcsvári d6ca125cac
docs: diagnose the Symbol Properties hang (stranded tool fiber)
Reproduced live on the dev platform: the tool fiber running the
quasi-modal parks mid-body, is quarantined by the stale-fiber guard,
and its resume is REFUSED — so it never releases the dispatch guard.
Interlock held forever => clicks deferred and never drained, timer
delivery frozen; the titlebar X works because it is ungated.
Includes the captured frozen state, what is ruled out (clicks do reach
wx; no I/O in flight), and ranked fix directions. Regression vs
pre-existing still undetermined — needs a real WX_SCHEDULER=0 build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
2026-08-10 10:14:14 +02:00

93 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 19 — Symbol Properties dialog hangs: a stranded tool fiber (live investigation)
> **Status: DIAGNOSED, NOT FIXED (2026-08-05).** Reproduced end-to-end in a real browser
> against the dev platform on the scheduler build. Regression-vs-pre-existing is **not yet
> determined** — see §5. Related: [`17`](17-mailbox-scheduler-plan.md) S4 (waits),
> [`16`](16-fiber-resume-guard.md) (the quarantine guard), the 8/4 three-UI-bugs triage
> (which blamed the interlock drain — that is the *symptom layer*, not the proximate cause).
## 1. Repro (100%, ~40 s)
Dev platform (`npm run dev`), editor at `:3048`, Arduino Leonardo schematic:
1. Open the project URL, wait for load (~30 s).
2. Double-click the USB receptacle (J1) → **Symbol Properties** opens.
3. Click OK / Cancel / any control → **nothing happens**. Only the titlebar × closes it.
## 2. Frozen state (captured live)
```
Fibers.__internallyParked = [607191040] // tool fiber, quarantined
Fibers.__parkSleepBuf = {607191040 → 607977472}
Asyncify.__pendingSleepContexts= [{buf 607977472, rootOwned:false, cleaned:false},
{buf 205586432, rootOwned:true, cleaned:false}]
Asyncify.currData = 205586432, state = 0
Fibers.__validSuspensions ∌ 607191040 // no resumable suspension
scheduler: mailbox=3 enqueued=655 delivered=652 // FROZEN (0 progress in 4 s)
waits=1 waitsBegun=1 waitsResolved=0 // the quasi-modal wait, unresolved
```
Console beacons, in order:
```
[wx-asyncify] overlapped-wake × 10 (benign: restore over null)
[wx-asyncify] aliased-wake-live: restoring currData=205586432 over 607977472
[wx-asyncify] fiber-resume-refused: fiber=607191040 is asyncify-parked mid-body (sleep in flight)
```
## 3. What is NOT the cause
- **Not clicks failing to reach wx.** The OK click produced exactly one
`wx_dom_event(domId 81, kind 1)` ccall; the button is enabled and hit-testable.
(Rules out the `WINDOW_DISABLER`/`IsEnabled()` hypothesis.)
- **Not slow I/O.** The last library network resource completed at t=5 s; the hang was
inspected at t=277 s with nothing in flight. The fiber's park is waiting on a promise
that will never settle — a **lost wake**, not pending work.
- **Not the mailbox/embind/wait bookkeeping.** `strayWrites=0`, `mutQ=0`, no deferred
wakes, no stranded messages beyond the 3 blocked by the interlock.
## 4. Mechanism
The tool fiber running the dialog parks mid-body (the quasi-modal wait). The stale-fiber
guard correctly marks it `__internallyParked` — its slice ended with a sleep in flight.
Its resume then arrives and is **refused** (`fiber-resume-refused`), because a quarantined
fiber has no valid suspension; the guard's contract is "the parked body completes via its
own wake." Here that wake *is* the refused resume, so nothing ever completes:
- the fiber never resumes → the dispatch guard it holds is never released →
- `wxWasmDispatchParked()` stays true forever → `ProcessEvents` is Paint-only and
`wxWasmMailboxDeliver` bails → **every** subsequent click is deferred and never drained,
and timer delivery stops (the frozen 652).
- The × works because `wx_window_close` (`toplevel.cpp`) is ungated and synchronous.
The 8/4 triage saw the *outer* ring of this (deferred clicks + a drain gated on the same
predicate) and proposed flushing the queue at depth 0 — that cannot help: the depth never
returns to 0 because the holder is stranded.
## 5. Open: regression or pre-existing?
Undetermined. The quarantine guard and the aliasing repair are pre-existing (ported
verbatim into the scheduler at S2), but S4 changed *how* a quasi-modal parks
(`wxWasmRunNestedLoop` pump → `wxWasmBeginWait`/`wxWasmYieldUntil`). Both shapes park the
fiber mid-body, so the quarantine interaction plausibly predates S4 — but that must be
proven, not assumed. **A hand-edited legacy glue is not a valid comparison** (attempted
2026-08-05: stripping the scheduler and re-injecting `handlesleep.js` into a C-lane build
booted to a fatal `TypeError: … reading 'mode'`). The differential needs a real
`WX_SCHEDULER=0` docker build of `kicad_editor`, same flow.
## 6. Fix directions (ranked, none implemented)
1. **Don't drop a refused resume — defer it.** When `__refuseFiber` fires for a fiber in
`__internallyParked`, record the pending resume and retry it when that fiber's park
resolves (`__parkSleepBuf` already maps fiber → its sleep buffer, and the sleep's
context leaving `__pendingSleepContexts` is the exact "now safe" signal). This is the
same drop→deliver flip the whole mailbox migration is built on, applied one layer down.
Caveat: doc 16 closed the *deferral family* for the root-hot case (a suspension broken
at write time). This is a different case — a healthy fiber quarantined mid-body — so the
closure does not automatically apply, but the round-6 evidence must be re-read first.
2. **Make a permanently-held interlock loud** (watchdog beacon after N seconds with a
parked holder). Diagnostic only, but it turns this class from "UI mysteriously dead"
into a one-line console verdict.
3. **Handler fibers** (the ledgered Design-B step): if parkable handlers own scheduler
contexts, "a parked chain holds the global interlock" stops being representable. This is
the structural cure and the S5 ledger's unlock for deleting the interlock entirely.