Commit graph pcbjam/docs/features/async
Author SHA1 Message Date
Gergő Törcsvári
84a40d4492
diag(asyncify): write-time instrumentation + local warm-load repro findings
The prod differential ladder finished: staged byte VOLUME on a warm load is
the only trigger left (V1a siblings-without-lib-tables dies, V1b +120 files
survives, V1c sibling KiCad files renamed byte-for-byte dies, V1d Leonardo +
123MB of inert markdown dies on loads 3-4; 14MB never dies). 3D models,
collab/ydoc/presence, lib tables, sibling KiCad handling and file count are
all exonerated — volume only loads the dice on the underlying race.

That made the crash reproducible locally for the first time in six campaigns:
a persistent browser profile + a 110MB project fails every warm load with the
exact prod signature. Iteration is now ~12 minutes instead of a release cycle.

Shim: every fiber switch now records the departing side's remaining asyncify
buffer and its recorded rewind entry (rem=/rf=), which is what identified the
unrewindable capture and disproved buffer overflow. The deferral family is
closed for good — a microtask-deferred retry on a clean empty stack died
identically to the nested rewind, because the suspension is broken at write
time, not by nesting.

Shell: log the origin stack when wx reports the top window destroyed. That
notification fires from ~wxTopLevelWindowWasm for ANY top-level window, so a
transient frame dying mid-load navigates the user out of the editor — a real
bug in its own right, found while chasing the empty flight-recorder dumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-03 11:05:10 +02:00
Gergő Törcsvári
f734d700a2
fix(asyncify): retire the deferral family — guard-layer road closed
v0.1.24 in prod, doubly convicted the same morning: the Leonardo open
crawled/hung (open:settled result=failed at the 60s escape, heap never past
256MB — every main-loop iteration runs INSIDE its yield-wake extent, so the
"root-owned wake" scope matched thousands of legitimate nested coroutine
Call/returns per open, each paying a deferred macrotask, throttled to ≥1s in
a background tab), AND the Nano crashed 22ms after deferrals=1 fired.
Harmful and insufficient: the fatal nested-rewind interleave and the benign
bulk are observationally identical at this layer — no discriminator exists.

Retired (second and final retraction, async/16 round 5). What stays shipped
and clean: consume-once root suspensions, the internally-parked quarantine +
laundering check, the flight recorder + beacons, the WSOD floor, the
pendingSleeps leak fix (confirmed by pendingSleeps=[] in the Nano dump). The
rare nested-rewind crash is ACCEPTED and fully observable until the
structural fix — the design-B fiber-first runtime (async/06,12,13), where
one scheduler owns every suspension and this interleave cannot exist.

.ci-cache-epoch 6→7.

Local: fiber 2/2 (one refusal beacon) + timer + firefox sweep 21 passed,
chromium scenarios 11 passed/4 quarantine-skips, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-02 08:18:08 +02:00
Gergő Törcsvári
210b079ed9
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured
The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8):
dozens of benign fiber round-trips at w=0, the yield cycling healthily on its
buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with
currData=root+20. The fatal condition, observed rather than inferred: a fiber
round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and
re-rewinds the root nested inside its live wake rewind. Consume-once passed
correctly — it guards a different corruption and stays.

The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes,
flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned
(fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned
wake; root entries don't count as slices); finishContextSwitch(root) defers
one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot).
Beacon: root-entry-deferred. Verified inert where it must be: zero beacons
across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress —
the single miss carried no beacons, i.e. the pre-existing under-load flake).

Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump
carried ~380 leaked zero-linked entries), and wake events in the recorder are
tagged R/f for ownership.

.ci-cache-epoch 5→6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:49:38 +02:00
Gergő Törcsvári
ae33a100c2
fix(asyncify): consume-once root suspensions — replaces the wake-window deferral
The deferral (9ca2ac1) modeled the wrong condition and taxed every parked
fiber completion with a macrotask hop: under CI load that stretched
three-client apply chains and flaked drift-trio S4 twice consecutively
(26/26 green locally under stress) — retracted.

The actual fatal state, readable in all four prod stacks once seen: a SECOND
rewind of the same root suspension. Root suspends once per fiber_swap out of
it; two parked fibers completing against one root suspension epoch (a tool
fiber + a collab fiber both waking around open:settled) each drive
finishContextSwitch(root) — the second rewinds already-consumed data →
"unreachable executed" → poisoned runtime, with the wake-side "index out of
bounds" as the sibling symptom.

Cure: stop exempting root from the validity check the shim already keeps.
First consumption proceeds synchronously — zero added latency anywhere; the
second is refused ([wx-asyncify] "root suspension already consumed") — the
yielded fiber stays properly suspended and resumable, root continues via its
real pending resume, libcontext's ghost-epoch contract enforced one layer
lower. Root remains exempt only from the internally-parked quarantine (its
yield park is routine).

.ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim).

Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios
kicad-chromium 26/26 under 3-worker stress, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 15:53:18 +02:00
Gergő Törcsvári
eff5befd5d
fix(editor): DOM-level blue-screen floor — React can no longer white-screen a crash
v0.1.22's WasmErrorBoundary was still not enough: a commit-phase throw in
WasmTool's OWN effects unmounts the root, and no boundary below it helps.
fatal-screen.ts is the floor: plain-DOM blue screen with its own mirrored
log ring (append feeds recordFatalLog), installed at module import in
main.tsx — before and independent of React. It cooperates with the React
overlay: hidden while [data-testid="fatal-overlay"] exists, takes over via a
1Hz ensure-loop the moment it disappears. Fatal promotions also append the
asyncify flight-recorder dump so whichever screen survives carries the
targeting data.

fatal-overlay.spec.ts now also rips out the React root after the fatal and
asserts the DOM floor takes over with the mirrored [fatal] log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:07:09 +02:00
Gergő Törcsvári
5333099810
test(kicad): poisoned-attribution lever — the laundering scenario, red/green
kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.

Doc: async/16 rounds 2 + WSOD section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:42 +02:00
Gergő Törcsvári
e561507898
fix(async): fiber resume guard — the prod board-load trap, red/green
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).

- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
  exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
  resume→sleep park→mid-park Resume, the exact prod state machine. The
  first yield matters: it primes a real (then stale) suspension, matching
  long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
  polled state only (embind returns across fiber swaps are unwind
  placeholders). RED on the unguarded build — fiber/sleep buffer
  cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
  the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
  park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
  pcbnew-collab, collab-undo, eeschema-collab (19 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 23:37:05 +02:00
Gergő Törcsvári
f86ef9e433
test(kicad): timer-park repro lever — deterministic concurrent-Asyncify window
kicadTestArmTimerPark(delayMs, parkMs): a one-shot wxTimer whose Notify()
emscripten_sleep()s, entering through the exact GAL-refresh-timer path
(emscripten_async_call → TimerCallbackFunc::Run → dispatch guard → Notify) —
the fresh-entry-that-parks the prod board-load trap family needs. Pollable
kicadTestTimerParkState(); inert unless armed. Registered beside
kicadTestSetOpenPark in pcbnew + the merged kicad_editor image.

tests/kicad/timer-park-repro.spec.ts drives four escalating cycles (park
only, 2× + fiber hammering, + 256MB heap growth mid-park) and asserts the
runtime survives every rewind AND that the [wx-asyncify] diagnostics observed
the window — engagement is asserted, so a run where the lever never created
the overlap cannot pass vacuously.

Result so far (docs/features/async/15-timer-park-repro.md): GREEN through
both rounds — genuine double-parks, live currData cross-restores, fiber
swaps, and mid-park heap growth are all handled by the shim + runtime. The
prod trap needs an ingredient this window still lacks (ranked in the doc);
the spec stays as the regression gate for whatever the eventual fix is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 20:34:32 +02:00
Gergő Törcsvári
a26ef4ebeb
fix(load): open-settle gate — kicadOpenFileBusy probe + collab entry guards for the parked-open embind trap (indirect call signature mismatch) + deterministic collab-load-fuzz e2e
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-30 14:17:48 +02:00
Viktor Vaczi
c1ef489cfa feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on)
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy
encoding) across the whole build, keeping Asyncify coroutines working via a
from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the
only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially.

Highlights:
- Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify;
  post-link Asyncify covers every app wasm (not just standalone test wasm).
- Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps
  with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports.
- libcontext fiber entry wired under native exceptions; while-loop main loop +
  currData shim injected into all wx apps.
- Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake
  C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch.
- 3D viewer enabled by default (real raytracer linked, not the stub).
- Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to
  shim-redundancy pins (native-EH stays clean with the legacy shims ablated).
- Fix the asyncify-races quiescence check to not require Asyncify.currData==0:
  under the native-EH per-frame-yield top loop the main stack is asyncify-suspended
  every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer,
  not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad.
- CI: drop the obsolete binaryen_version input/env (the build uses the binaryen
  submodule fork's wasm-opt, not a version download); key the wasm-output cache on
  the binaryen submodule SHA instead.

Bumps the wxwidgets + binaryen submodules to their squashed feature commits.

Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63
Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines;
asyncify 7/7 both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:40:26 +02:00
Istvan Matejcsok
5d18bd0c45 test: 💍 deadlock test + readme update 2026-06-19 09:22:58 +02:00
Viktor Vaczi
fa408c0c06 fix(wasm): bump wxwidgets for the wxMenuBar Detach() UAF fix
Submodule pointer bump for the wxMenuBar dangling-child fix that resolved the 18 DOM-port kicad e2e failures. Also adds the async-debug dossier (docs/features/async/08-10) root-causing the 'indirect call to null' / 'signature mismatch' family to the menu-bar use-after-free, and folds the asyncify harness into 'npm test' (new test:wx for wx-only); tests/README + WHATWORKS updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:10:05 +02:00
Viktor Vaczi
14ca16cbd3 test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs
The asyncify single-slot work, executed red-green (full ledger:
docs/features/asyncify-arbiter/redgreen.md; decisions record:
docs/features/async/07-decisions-and-outcome.md):

- tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated
  playwright config: 8 scenarios reproducing the KiCad asyncify failure
  family with the kicad-faithful startup topology (pre-park fiber swap →
  park throw through the live trampoline). Built in 3 variants; the
  SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds
  keep the historical hang and index-out-of-bounds crash reproducible
  forever (mutation-style pins for the existing shims).
- scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel
  in the wakeUp path — when main's last pre-park suspension was a sleep,
  the main-loop park throw escaped through that sleep's promise reaction
  as an uncaught rejection (the calculator/gerbview console errors).
- scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs.
- Spec tightening (the acceptance bar): 'uncaught exception: unwind'
  tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard
  clean-console gate over 5 asyncify corruption signatures.
- wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error,
  sync clipboard IsSupported (014f67e6c1).

Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40
passed / 2 skipped with ZERO corruption signatures in any log across all
six apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:02:59 +02:00
Viktor Vaczi
66ce367703 docs: asyncify currData + wasm-exceptions research dossiers
Two research dossiers under docs/features:
- async/: Asyncify currData contention — the clipboard crash and post-idle
  tool hang as one disease (single global suspension slot, three producers),
  asyncify internals walkthroughs, prior art, arbiter designs A/B.
- wasm-exceptions/: -fexceptions vs -fwasm-exceptions — measured invoke-driven
  share of the asyncify tax on pcbnew (59% raw / 64% gzip; download 64.5->36 MB
  if migrated), KiCad dialog-in-catch audit (85 direct / 93 review of 636,
  catch_audit.py included), binaryen toolchain status (partial EH support since
  v125; TryTable unsupported), and a catch-arm-hoisting pre-pass design that
  would obsolete the KiCad-side refactor. Cross-linked with the parked
  KICAD_WASM_EH end-to-end experiment (docs/wasm-exceptions-experiment.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:02:59 +02:00