Commit graph pcbjam/wasm/bindings/collab_common.h
Author SHA1 Message Date
Viktor Vaczi
9c475a804e jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.

Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
  could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
  (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
  WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.

Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
  workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
  of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
  scaffolding (existed to parallelize the deleted wasm-opt phase; the
  postprocess is a seconds-long node script and now runs inline),
  build-monitor's dead asyncify rows, sched-context orphan build
  output, dead .gitignore entries, the .jspi-assets spike dir (the two
  wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
  called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
  main_stack_runner.h + 5 includes, the always-null context-sleep weak
  hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
  noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
  WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
  (gerber-demo keeps it: it loads the deployed CDN release, which
  predates emscripten 6 — noted inline).

Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).

Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).

Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).

Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).

Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
Gergő Törcsvári
64d2cdf259
Revert "fix(wasm): never swap to a fiber while an Asyncify context is parked"
This reverts commit 32218122c3.
2026-07-29 17:29:28 +02:00
Gergő Törcsvári
32218122c3
fix(wasm): never swap to a fiber while an Asyncify context is parked
Opening a large board could kill the editor runtime outright: "index out of
bounds" / "unreachable executed" / "indirect call signature mismatch", after
which every later focus/key event trapped. Reported in prod against a big
uploaded board, and correlated by the reporter with the moment the presence
WebSocket connects.

That correlation is the tell. Presence/collab work enters the wasm through
runOnFiber -> COROUTINE::Call, i.e. emscripten_fiber_swap, whose stop_unwind
corrupts Asyncify's single currData slot when ANOTHER context is already
parked there. docs/features/async/13 pins the invariant: exactly one
unwind/rewind transition in flight, and prescribes "a single shared is-a-
transition-in-flight guard the pumps consult before re-driving".

Normally the slot is free when fibers drain: the main loop's per-frame
wxWasmYieldToBrowser completes every frame, so drainFibers runs between
yields. It is NOT free when a nested/modal pump tick drives ProcessEvents
while the chain that opened the modal is parked deeper down — precisely a big
board open (progress dialog over a parked load). The wx dispatch interlock
does not cover this: the modal parks deliberately zero its count so their own
pump may dispatch.

So gate the swap itself: drainFibers defers (re-CallAfter) while
asyncifyInFlight(). Bodies are viewport/overlay/apply work, so waiting out the
park costs latency, never correctness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8jo7zz1ZwzYpjJ64UZKN4
2026-07-29 15:29:36 +02:00
Gergő Törcsvári
22cd32b7b2
fix(drift-trio): pin fiber slot across asyncify parks (#10b layer 1)
Symbolized (HOIST_KEEP_NAMES=1): the trap is on the asyncify REWIND re-entering
the fiber — stack-local COROUTINE+body were destroyed when Call() returned
early on a park, so the rewind called through freed objects (latent UB in the
ORIGINAL fire-and-forget runOnFiber too). Heap-pinned FiberSlot + explicit
done flag + fiber-tail re-drain. Layer 2 (rewind interplay) still open —
fuzz stays fixme'd; pageerror stacks now captured in fuzz artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 12:45:51 +02:00
Gergő Törcsvári
3fc90e8fe2
fix(drift-trio): phase E — serialized fiber queue (#10a) + fiber-busy probes
runOnFiber now runs bodies strictly one-at-a-time through a park-safe FIFO
(collab_common.h): the per-body fire-and-forget coroutine interleaved under
load — an asyncify park inside commit.Push let the event loop start the next
body, so a local commit and a remote apply ran interleaved on shared state
(s_applyingRemote is one global), silently losing applies on the actively-
editing receiver (fuzz finding #10a; B now fuzzes clean; 39-test suite green).

kicadCollabFiberBusy embind probe (merged + standalone registrations): a
bare-embind-stack scratch save during a parked fiber mis-dispatches (table
index OOB) — trio.ts modelText/drift and production drift-detect now defer
while fiber work is in flight (#10b hardening; the trap's root cause is still
open and needs a symbolized stack — fuzz stays fixme'd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 12:22:09 +02:00
Gergő Törcsvári
855137c650
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):

- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
  fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
  (onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
  hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
  state and machinery (start/canvas binds/lock query, setRemote/setPins/
  setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
  releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
  SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
  selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
  (anonymous-namespace presenceCore()) so the merged image keeps per-editor
  state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
  the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
  duplicated within each TU, letting the test probe drift from the pixels.

Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).

eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.

Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-10 09:17:32 +02:00