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
This commit is contained in:
parent
9bc4da89ff
commit
d6ca125cac
2 changed files with 280 additions and 0 deletions
93
docs/features/async/19-quasimodal-fiber-strand.md
Normal file
93
docs/features/async/19-quasimodal-fiber-strand.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# 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.
|
||||
Loading…
Reference in a new issue