findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work - #40
Open
matejcsok-pcb wants to merge 6 commits into
Open
matejcsok-pcb wants to merge 6 commits into
matejcsok-pcb wants to merge 6 commits into
Conversation
…rror + abortable model prefetch Adapted from codex/asyncify-execution-owner-core 3753320 (scheduler-free on that branch already; one comment line re-worded for the JSPI line): - E-1: generation-slotted WorkerSlot with a 2-min boot watchdog (armed before resolveWasmBase, so a hung delivery lookup expires too) and a 30-min per-request response watchdog; retireWorker() is the single idempotent funnel (fail that generation's pendings, terminate, revoke the worker Blob URL, clear timers/listeners). - E-2: worker.onerror is wired for the worker's whole life and settles every in-flight STEP/export request; a synchronous postMessage throw settles its request without leaking the pending id; late frames from a retired generation are inert. - E-3: worker.onmessageerror retires the generation like error does. - E-4: collectBoardModelFiles is a pure source/IDB/network path (no editor MEMFS round-trip) taking an AbortSignal checked at every loop head; prefetchBoardModels races it against a 30 s deadline — timeout is non-fatal (export proceeds without models) and late results are inert. Tests: occ-service.test.ts (7, ported) — boot/response watchdog expiry, crash-settles-all, bootError retry, decode-fault retirement (invokes the real onmessageerror transition, per J-4), hung-prefetch export; models-bridge.test.ts +3 — abort inertness, zero FS access on the collect path. e2e harness twin updated to the same generation shape (adds __occServiceTestHooks/failDecode). False-green audited: 19 cases fail with the fixes reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed event transport Adapted from codex/asyncify-execution-owner-core 3753320. Service side mirrors the occ-service shape (E-1 watchdogs, E-2 fail-all + boot-death fix — onerror now rejects the in-flight boot waiter instead of stranding it, E-3 onmessageerror terminal, Blob URL revoked, per-generation evtQueue cleared on retirement). E-6 transport bounds (worker hunks re-applied inside the emscripten-6 em-pthread else-branch — the codex file predates that split, so this is a re-application, not a cherry-pick): - batch cut at 512 lines / 1 MiB exact JSON-UTF-8 bytes, measured before a line is retained; a single line > 1 MiB flushes the accepted prefix then stops the event stream terminally (never retained); - posting gated by a 64-frame / 8 MiB unacked credit window; each frame carries { eventSequence, eventBytes } and is released only by an exact { sequence, bytes } ack; any mismatched ack is terminal; - the service mirrors the same 64-frame / 8 MiB bound on its pre-handler queue, acks after handing a frame to __ngspiceOnEvent, and retires the generation on invalid credit; { fatal } frames retire the worker. Tests: ngspice-service.test.ts (11, ported) — watchdogs, crash/bootError/ decode-fault settlement + recovery, out-of-order ids, sync postMessage throw, stale-generation event drops, fatal-frame retirement. tests/tools/ ngspice-worker-batch-unit.ts (node:vm over the production worker source; `npm run ngspice:worker-batch`) — bounded ordered chunks, byte-pressure flush, 100k-chunk credit storm, over-limit line, exact ack lease. e2e harness twin updated to speak the ack protocol (adds __ngspiceServiceTestHooks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion admission gate
E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped
execution owner; under JSPI a fresh non-suspending JS→wasm entry while
another activation is suspended is structurally safe on its own stack, so
the admission boundary for worker completions is liveness + trap state, not
execution ownership):
- jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`),
canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError +
cross-realm string classification), and runWaitCompletion(site, token,
prepare, inertResult): prepare runs immediately and owns ALL native work;
stale tokens and dead/terminal instances drop loudly without resolving
(resolving would resume the parked frame inside the damaged module); a
trap latches terminal; a plain JS bug resolves inertResult so the wait
fails instead of stranding. beginWait refuses (token 0) when dead/terminal.
- all four delayed completion sites route their native work through the
gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion'
(oce_plugin_stub — the MEMFS cache write moves inside the gate too),
'ngspice request completion' and 'ngspice vector completion'
(sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare,
inertResult 1 = transport error). Every wxWasmBeginWait caller in the
stubs bails on token <= 0.
- deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion,
executionBarrier, the byte-credit native-entry FIFO — completions are
one-shot per wait token and stream volume is bounded at the E-6 transport
credit window. Cross-refs logged for group M (M-2/M-6/M-8).
E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8
gate): js_ngspice_install_events binds the handler to the EXACT installing
module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity),
re-installation replaces a foreign module's handler, a superseded handler
disarms itself, native entry goes through installingModule._malloc/
._pcbjam_ngspice_event (never lexical Module), each dispatch checks
canTouchNative() (loud drop on a dead/terminal module), and a trap on the
per-line entry latches the terminal gate.
Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/
js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export
decode-fault recovery (real onmessageerror transition via failDecode, J-4),
ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7
applied-generation receipt (codex's executionBarrier await replaced with a
pendingWaits('ngspice') drain poll — the JSPI-line equivalent).
Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rted shapes E-5: the module-identity bridge called installingModule._malloc, but this build exposes _malloc only as a bare glue-closure export (Module._malloc is absent) — every char/stat event entry threw TypeError, which also starved the E-6 credit window (thrown dispatches never acked) and wedged the queued bg-finished frame behind them. The bridge now uses the bare closure exports (identity is still exact: the EM_JS body IS the installing module's closure; the __ngspiceOnEvent self-disarm covers supersession). E-6 (codex reference design corrected — its validation matrix never ran): a FULL credit window was terminal (stopEventStream at 64 in-flight frames). Under live e2e that killed a real simulation: bg-thread emissions proxy one per task, so each line ships as its own frame and a normal transient outruns a busy main thread. A full window now DEFERS into a bounded FIFO (512 events / 4 MiB) drained in order as acks free credit; only true overload or an invalid ack is terminal. Retention stays bounded (8 MiB in flight + 4 MiB deferred + 1 MiB open batch). And the service/harness mirror queue now acks at ENQUEUE — placing a frame in the bounded pre-handler queue is taking ownership; without that, a stream starting before the C++ handler installs (the ngspice-probe page) starves the worker window forever. Test updates: worker-batch reducer — new "a full credit window defers and drains in order, never terminal" case pinning the regression; the storm case now proves the deferred caps are the terminal edge. board-ready.ts gains the owner-free openBoardProgrammatically (codex helper the ported occ-export spec needs; the barrier-based waitForUiBoardReady was NOT taken). occ-export.spec: domId is optional on this line's registry (coordinate fallback is the supported path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fdef bump Coverage audit found three fixes without a test that could catch their reversion: - E-6 ack-on-enqueue: new unit case "acks a pre-handler queued frame at enqueue so the transport window never starves" (ngspice-service.test.ts; false-green audited — fails with the ack removed, exactly one ack per frame, none repeated on drain). - E-5 / E-8 wiring / E-9 / E-7 shape: new source-contract tripwire tests/tools/findings-e-source-contract.ts (npm run findings-e:contract) — the codex-thread contract style for C++/EM_JS code that cannot be behaviorally unit-tested without a wasm build. Asserts: the module identity stamp + self-disarm and ABSENCE of the install-once presence guard (E-5); all four completion sites use runWaitCompletion, no stub resolves a wait directly, token<=0 bails, the shim exports the gate surface (E-8); the run-generation mechanism present with its behavioral drops confined to the wasm build (E-7); the identity-checked destructor unregistration (E-9). Also bumps kicad to the E-7 ifdef-confinement commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
….E-9 work
A review of the group-E fixes found 13 further defects; ten were introduced by
those fixes, two pre-existed and were merely relocated, one is deferred.
Services / transport
E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning
mirror stayed latched true after a mid-run worker death: Run stayed
disabled and the promised fresh-worker restart was unreachable for the
whole session. Retirement now dispatches a synthetic controlled-exit
straight to the installed handler (never through dispatchEvt — a
fabricated frame must not touch the credit ledger). Driving the repro
exposed two further defects, both fixed here: a replacement worker
trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit
that uninitialized engine before KiCad's validate() re-init (the native
flow assumes a crashed engine survives in-process — true for the dll,
false for a dead worker). Reads now answer their empty shapes pre-init,
writes lazy-init, and init is idempotent per worker engine.
E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice
client deliberately rethrows non-trap errors — so each throw leaked one
unit of the 64-frame credit window until the stream died with a
misattributed "transport exceeded". The ack moves to a finally in both
service copies; the throw still propagates (the trap machinery needs it).
E-20 the oversize-line path promises to transfer the accepted prefix, but
with the window full that flush only DEFERS, and stopEventStream wiped
the deferred queue — losing the diagnostics that explain the failure.
The terminal notice now carries them as pendingEvents; both hosts
deliver them in order, unacked (the fatal frame is outside the credit
protocol).
E-21 the 30s prefetch deadline discarded every model already collected and
reported nothing. A caller-owned progress sink ships the partials and
the omission reaches the export report. (Awaiting the aborted collection
was rejected: an in-flight source fetch is not abortable — E-4's
original disease.) Plus a serving-candidate memo, so a .wrl ref served
by its .step fallback stops re-probing the miss on every export.
Scheduler
E-14 _terminalizeNativeTrap classified by message substring, so any plain JS
error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a
healthy instance. Now structural only: instanceof RuntimeError plus a
duck-typed name check (verified in this build's glue that abort() throws
a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort
now latches the gate — the authoritative notification, previously
ignored.
E-15 the shim half: _pumpResume gates on terminal (catching wakes already
queued at latch time) and resolveWait refuses on terminal WITHOUT
consuming the entry, so a frame stays visibly parked rather than
resuming inside a trapped module.
E-16 the E-5 handler read the realm-global scheduler at dispatch instead of
its installing module's; also frees the per-line buffer on the non-trap
rethrow path.
E-11 get_vec trusted the worker's res.length over the transferred arrays.
Observed death shape: a 4 GiB std::vector threw an unhandled
std::length_error that exited the editor's main loop. Now clamped, with
the buffers freed on every failure path.
Guardrails (replacing two deferred refactors: e2e→production-code injection and
collapsing the four copies of the worker-lifecycle machinery)
E-18 the source contract asserted comment-string counts — rewording failed
CI while moving a guard outside its #ifdef passed. It now parses the
#ifdef regions and asserts on code.
service-stub-parity.ts pins what the four lifecycle copies must share:
credit-window equality parsed from source, the finally-ack, boot
deadlines, terminal-notice consumption. The transport numbers are now
single-sourced from the worker.
CI actually runs the gates: the web/standalone vitest suites (which had
NEVER run in CI), the reducer, the source contract and the parity tool —
with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint.
E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in
the ngspice-stub shape with a wedgeNextBoot() repro hook.
Every behavioral fix has red-then-green evidence (the reds were captured first).
E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's
transport deferral) is DEFERRED with its analysis recorded — a real fix needs
run identity on the bg frames.
Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare
markers carry their reason; three export-report dwells became modal-lease polls;
exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch,
an unused fault hook and unused receipt plumbing are gone; abort scans, wx
dialog drivers, the sim harness and the vitest FakeWorker are each one copy now.
Bumps kicad and wxwidgets to their findings-group-e tips.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A code review of the group-E fixes (E-1..E-9, landed 2026-08-19) found 13 further defects. Ten were introduced by those fixes, two pre-existed and were only relocated, one is deferred. Sibling PRs: wxWidgets#5, kicad-source-mirror#9, pcbjam-private.
Services / transport
E-10 — Run died for the whole session after a worker death.
retireWorkersynthesized no bg/exit frame, so sharedspice'ss_bgRunningmirror stayed latched true:ENABLE(!simRunning)kept Run disabled and the promised fresh-worker restart was unreachable. Retirement now dispatches a synthetic controlled-exit straight to the installed handler — never throughdispatchEvt, since a fabricated frame must not touch the credit ledger.Driving that repro end to end exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's
cm_input_path/circhit that uninitialized engine before KiCad'svalidate()re-init. The native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker. Reads now answer their empty shapes pre-init, writes lazy-init, andinitis idempotent per worker engine.E-19 — one throw leaked one credit unit.
dispatchEvtacked only afterhandler(evt)returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked a frame of the 64-frame window until the stream died with a misattributed "transport exceeded". The ack moves to afinallyin both copies; the throw still propagates, because the trap machinery needs to see it.E-20 — the terminal stop discarded the diagnostics. The oversize-line path promises to transfer the accepted prefix, but with the window full that flush only defers, and
stopEventStreamwiped the deferred queue — losing the very lines explaining the failure. The fatal notice now carries them aspendingEvents; both hosts deliver them in order, unacked (the fatal frame sits outside the credit protocol).E-21 — the prefetch timeout was all-or-nothing and silent. The 30 s deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. Awaiting the aborted collection was rejected: an in-flight source fetch isn't abortable — E-4's original disease. Plus a serving-candidate memo, so a
.wrlref served by its.stepfallback stops re-probing the miss on every export.Scheduler
E-14 — a message substring could brick a healthy instance.
_terminalizeNativeTrapclassified by text, so any plain JS error quotingAborted(orout of boundslatched the one-way terminal gate permanently. Now structural only:instanceof RuntimeErrorplus a duck-typed name check — verified in this build's glue thatabort()throws a genuineRuntimeErrorboth pre- and post-runtime-init, so the message arm bought nothing.Module.onAbortnow latches the gate; it was previously ignored entirely.E-15 (shim half) —
_pumpResumegates onterminal, catching wakes already queued at latch time, andresolveWaitrefuses onterminalwithout consuming the entry, so a frame stays visibly parked indump()rather than resuming inside a trapped module.E-16 — the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path.
E-11 — a corrupted worker answer killed the editor.
get_vectrustedres.lengthover the transferred arrays. Observed death shape: a 4 GiBstd::vectorthrew an unhandledstd::length_errorthat exited the main loop — nastier than the predicted OOB trap. Now clamped, with buffers freed on every failure path.Guardrails
Two review recommendations were deferred by maintainer decision — making the e2e suite drive production service code instead of the stub mirrors, and collapsing the four copies of the worker-lifecycle machinery. These stand in:
service-stub-parity.tspins what all four copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. Transport numbers are single-sourced from the worker.#ifdefpassed. It now parses#ifdefregions and asserts on code.web/standalonevitest suites (which had never run in CI), the reducer, the source contract and the parity tool — with aNON_PLAYWRIGHT_GATEScheck so deleting a step re-fails the lint.a66e109, restored with awedgeNextBoot()repro hook.Deferred
E-17 — a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral. Analysis recorded; a real fix needs run identity on the bg frames. Explicitly not "not-a-bug".
Verification
Every behavioral fix has red-then-green evidence, reds captured first. Vitest 404/404; reducer 9/9; source contract and parity tool green with false-green audits in both directions; clean kicad suite (both engines) 348 passed / 1 pre-existing marginal dark-mode rendering-ratio failure (0.2004 vs 0.2; nothing here touches rendering).
Live-collab gate passed on the real dev stack: a real UI tool dispatch moved a board item exactly 5 mm, a dialog parked the opener on an E-15-guarded
nestedwait and resolved cleanly on Cancel, and both clients stayed non-terminal with clean rings and zero parked waits across all nine kinds.Note for CI:
wx-chromium,jspi-firefoxandcoroutine-firefoxcannot run locally in this worktree — the standalone wx test apps aren't built here — so CI is their first real coverage.🤖 Generated with Claude Code