pcbjam/tests/kicad/3d-viewer-deadlock.spec.ts

250 lines
15 KiB
TypeScript
Raw Normal View History

import { test, expect } from './fixtures';
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate Squash of experiment/ff-big-modules vs main. Big-module routing removed: native-EH shrank kicad_editor below SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT crutch are gone — kicad-firefox and kicad-chromium both run the full suite, with the module compiled the way real users' browsers compile it. Per-engine screenshots end to end: stableShot/shotPath write test-results/<engine>/<name>.png; baselines move to baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on <engine>/<name>. Previously Firefox and Chromium renders of one spec overwrote each other and Firefox renders were never actually gated. Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium web-suite shots); manifest generated from the baseline tree. One merged playwright.config.ts (kicad/asyncify/coroutine/perf as projects); ~25 dead npm scripts dropped. The web suite is gated in CI for the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after npm ci; last 26 blind-sleep violations fixed. SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe (--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader transiently failed the first post-board-load draw and the recovery cascade ended in a silent permanent Cairo fallback — that engine flip was the "~1.2% changedRatio both directions" occ-export baseline flake. Validated 160/160 across two 80-repeat rigs; full analysis in docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium baselines shift slightly on llvmpipe — promote once from the first green run. Deflakes the new coverage exposed: presence baselines settle before capture; presence fixtures declare current file formats; perf gets its own outputDir so CI evidence survives; occ-export settles the board paint before the export dialog; menu-item waits (waitForRenderedByLabel before clickMenuItem) in 4 specs + the TESTING.md rule. Web suite runs the PROD build, in parallel: webServer becomes backend `start` + the standalone's e2e:preview (build-preview.mjs: link-wasm → stash the public/wasm symlink aside during vite build, build-demo.mjs's move — then vite preview as the persistent server). The wasm middleware serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread worker script's own response must carry COEP or Chrome kills it with ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time; VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed: shared-page specs become serial groups; locks.spec grabs alice's exact item via the new kicadCollabTestSelectByUuid hook (cross-tab "first footprint" order is not a ysync invariant); quit specs poll page.url() (quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox). Suite: 51 passed / 12 skipped / 0 failed in 1.6m. CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be reachable from the npm scripts the workflows invoke — scraped from .github/workflows/, resolved through package.json, coverage asked from playwright --list itself. Rules: uncovered-spec + orphan-project (with a documented LOCAL_ONLY_PROJECTS allowlist). Gating next to lint:determinism; 138 spec files / 13 projects accounted for. Product fixes kept from the investigations (reachable on real GPUs too): wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush + per-instance font texture + first-frame GL-error drain (GAL recovery recovers instead of falling back to Cairo) + the user-facing eeschema switch navigates again under __EMSCRIPTEN__ (project-sync's FaceRegistered gate had rerouted it into the hidden sync player; caught by the newly-gated web suite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
import { clickByTooltip, clickToolbarTool, shotPath } from '../e2e/utils/element-tracker';
import { waitForPcbnew } from './utils/pcbnew-ready';
fix(ci): serialize the heavy 3D-viewer e2e specs (dedicated chromium-ci-3d project) The three heavy 3D-viewer specs (3d-viewer, 3d-viewer-deadlock, 3d-viewer-models) crashed on CI (run 28604015154): "Target crashed", "browser has been closed", and a black frozen canvas. Each boots the 3D-enabled pcbnew build, which pre-warms ~hardwareConcurrency*2+8 (~68 on the 30-core VM) Web Workers AND runs a multi-threaded CPU raytracer over SwiftShader software-WebGL. Under fullyParallel the three spec FILES run in separate browser processes concurrently, and the simultaneous raytrace tabs exhaust the shared GPU process's ~16 live-WebGL-context limit / renderer memory. The pool pre-warm (the deadlock fix, 7630c7e) is correct and present in the tested binary; per-file isolation prevented in-process Worker accumulation but NOT this cross-process concurrency — the gap these specs' first CI exposure hit. Fix (test-orchestration only; no wasm/submodule changes): - Route the three specs to a dedicated `chromium-ci-3d` Playwright project (own outputDir=pw-artifacts/kicad-3d so its failure traces survive the later perf run's wipe; the existing tests/pw-artifacts/** upload already captures it). - test:kicad:ci runs it as a SECOND `playwright test` pass with --workers=1, so at most one raytrace tab is alive at a time (the environment in which a single 3D load is reliably green — docs/features/3d-raytracer/README.md §7). An exit-code-aggregating sequencer runs both phases and fails if either fails (no silent skip of the 3D pass). - Cap phase-1 CI workers to 12 as headroom under the WebGL-context cap. - Log a resource-diagnostic snapshot (hardwareConcurrency, pthread pool, GL-canvas count, wasm/JS heap) before each 3D interaction so any recurrence is attributable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:49:23 +02:00
import { countGlCanvases, loadBoard, logThreeDDiag, openThreeDViewer } from './utils/threed-viewer';
/**
* Regression for the camera-move-on-canvas raytrace DEADLOCK.
*
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
* Since the 3D-webgl port the wasm viewer DEFAULTS to the real OpenGL renderer, which never
* had this deadlock so this spec explicitly flips the viewer to the raytracer engine (its
* toolbar toggle) before driving it; the raytracer remains user-reachable via that same
* toggle, so the regression it guards is still live product surface.
*
* The 3D viewer's EDA_3D_CANVAS is a wxGLCanvas whose paint (on the raytracer engine) runs
* the multi-threaded CPU raytracer. Moving the camera makes the raytracer spawn raw std::thread workers; with no
* PROXY_TO_PTHREAD its join runs on the browser main thread, and KiCad's own thread pool has
* already drained emscripten's pre-warmed Worker pool so those threads fall back to on-demand
* `new Worker()`, whose boot handshake needs the main thread back in the JS event loop, which it
* can't reach while blocked in the render's busy-wait join. That circular wait is the freeze the
* user hit ("move the model → tab freezes"). The fix pre-warms PTHREAD_POOL_SIZE =
* hardwareConcurrency*2+8 for 3D-viewer builds (scripts/kicad/build-kicad-target.sh) so on-demand
* creation never happens; a wxwidgets wasm-layer change (src/wasm/app.cpp) additionally defers the
* 3D viewer's synchronous mouse-button Paint to the yielding pump (jank + defense, mirroring the
* resize-deadlock fix).
*
* Drives the REAL viewer with two camera-rotate drags (mirroring "move the model move it
* again"), each followed by a settle for the raytrace to converge, asserting after every step that
* (a) the wasm main thread stays responsive (a deadlock hangs it) and (b) nothing aborted. Pre-fix
* a camera drag freezes or aborts; post-fix both stay live and the board keeps rendering. Frame
* drag/resize of the viewer are covered by 3d-viewer.spec.ts.
*
* ISOLATED in its own spec file (own Playwright worker own browser process a SINGLE heavy
* pcbnew load). The pre-warmed pool is ~2x hardwareConcurrency Workers PER load; running several
* 3D-viewer loads in one process (a serial describe) accumulates enough Workers that a later
* load's pool is short and the raytracer deadlocks anyway so this test must not share a worker
* with the other 3D-viewer tests.
*
* Notes: the glcanvas client area is pointer-events:none, so a page.mouse drag over it falls
* through to the main #canvas whose Emscripten mousedown/up callback dispatches into
* wxApp::HandleMouseEvent the deadlock path. WebGL pixels are read via drawImage2DgetImageData
* (preserveDrawingBuffer=true), since a CDP screenshot of a WebGL canvas is blank on swiftshader.
*/
test.describe('3D viewer camera-move deadlock', () => {
// CI-skip: this test asserts the wasm main thread stays responsive within ~15s DURING a
// raytrace. That premise only holds on a real GPU (fast render); on CI's software WebGL
fix(ci): 3D specs back to headless SwiftShader + GPU hardening; skip real-GPU drags on CI and the models render tail everywhere (product bug) The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With --use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the GPU-less CI VM: runs 28652367347 + 28664038296 both log `glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL canvas fails to create) and every failure is the FIRST viewer-open wait timing out. "Timeouts instead of crashes" meant no GL context existed to crash, not that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox project, but not for Chromium on this image. Headless SwiftShader is the environment where GL provably works — the serialized run 28649537489 opened the viewer and rendered/sampled 56 colours (viewer:26/130/209 all passed single-tab). Its remaining failures split into two families, each addressed on its own evidence: 1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame): - chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus --disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn stalls the software-GL GPU process until the watchdog shoots it (CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog off a slow op completes, and without the crash limit a lost GPU process keeps restarting for later tests. Same args locally (no-ops on a healthy real GPU). - Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas + JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock spec's 1.5s-interval settle polling. Sampled pixels and sig order are unchanged. - edge-resize spec: keep the open + 5-resize-handles assertions on CI, then test.skip the drag — each resize step re-raytraces synchronously on the wasm main thread and the 12-step drag blocked mouse.move past the 240s budget even single-tab. Real-GPU-paced; still runs locally (deadlock spec's CI-skip from d9900fe unchanged, same rationale). 2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI): raytracing a scene WITH component models kills the Chromium renderer process outright ~6s after scene build — silent process death with a FLAT wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error, no wasm abort, no crash report. Deterministic on a real GPU (5/5), fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically); the same death hits CI SwiftShader at ~66s. Board-only raytraces complete and stay alive, so the bug is specific to the model path. The spec's one historical green ended at raytrace-age ~6s — inside the death window by luck; every CI run of it ever died. Fix here: front-load the bridge assertions (ensure requests fire during scene BUILD, before the raytrace), so protocol regressions still fail the suite everywhere, and test.skip the render tail unconditionally with the bug documented at the skip site. The raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only render. Bug tracked for a dedicated fix session. Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock skip reason). Expected CI matrix: viewer open/render/stacking/titlebar + models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI by the standalone wx harnesses. Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped (the documented models render tail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:40 +02:00
// (headless SwiftShader, GPU-less contended VM) a legitimately-slow raytrace is
// indistinguishable from a deadlock, so the test can't run meaningfully there. The
// pthread-Worker-boot deadlock MECHANISM it guards is already covered on CI by the
// standalone wx harnesses (coroutine-pthread-ondemand, raytrace-threads). Runs locally
// on a real GPU.
test.skip(!!process.env.CI, 'raytracer liveness assertions require a real GPU; deadlock '
+ 'mechanism is covered on CI by the standalone coroutine-pthread-ondemand/raytrace-threads harnesses');
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
// KNOWN ISSUE (2026-07-04, webgl-era wasm build): the "Use raytracing" toolbar toggle is
// INERT — the click lands and a scene reload fires ("Reload time" status updates), but the
// canvas pixels never change (sampled for 20s; this spec's engine-engagement guard below
// caught it). Suspects: EDA_3D_CANVAS::DoRePaint's silent catch(runtime_error) freezing
// the canvas after a raytracer Redraw throw, or the engine toggle writing
// m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>() — possibly
// different instances in the merged kicad_editor bundle. Unskip once the toggle works:
// the engagement guard below then validates the engine flip loudly.
tests: un-skip sweep — 26 tests revived on the JSPI build, failures re-gated with fresh evidence Empirical pass over every skip/fixme whose premise the JSPI migration could have changed. Revived (verified green): - Firefox wasm-budget guards RETIRED (drift-trio, drift-trio-fuzz, drift-trio-scenarios, ysync-two-tab, ysync-libsymbols): the JSPI build (~half the asyncify size) fits three editor tabs inside Firefox 153's per-process budget — +20 firefox collab tests. - roundtrip 'pcbnew preserves items through a yjs round trip': the asyncify-fragile envelope parse it waited on is gone — both engines. - drift-trio-scenarios S4/S4b: converge now (was KNOWN ~5-8%). - pcbnew-collab + eeschema-collab 'a local move propagates A→B'. - web eeschema-fp-selector, read-only-editor's fixme'd writer-stream test, footprint-browse-remote read path (chromium; firefox gated: FootprintEnumerate rows never appear in 60s — slow wasm tier suspected). Still broken, re-gated with re-verified reasons: - 3d-viewer raytracer engine toggle: still inert, both engines. - maximize display geometry: wxDisplay reports 0x0 in the harness. - web editor WRITE bridge (symbol/footprint × remote/spike): wedges at the New Symbol/Footprint dialog step on both engines — the web-e2e-rot 01 gap stands for writes. Verify runs: kicad+wx touched files 93 passed / 0 failed / 7 skipped (intended gates); web touched files 7 passed / 0 failed / 9 skipped. lint:determinism + lint:ci-coverage green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 18:43:36 +02:00
test.skip(true, // re-verified still inert 2026-08-13 (both engines, JSPI build)
'KNOWN ISSUE: the raytracer engine toggle is inert on the webgl-era wasm '
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
+ 'build — the deadlock mechanism cannot be driven until it works (see comment)');
// One 187 MB wasm runtime is already heavy; keep this serial and generous.
test.describe.configure({ mode: 'serial' });
test.setTimeout(240000);
test('camera-move drag on the 3D canvas does not deadlock the raytracer (regression)',
async ({ page, testLogger }) => {
await page.goto('/kicad/pcbnew.html');
await waitForPcbnew(page);
await loadBoard(page, testLogger);
const winsBefore = await page.evaluate(() =>
Array.from(document.querySelectorAll('#window-container [id^="window-"]')).map((e) => e.id));
const glBefore = await countGlCanvases(page);
await openThreeDViewer(page, glBefore);
const winId = await page.evaluate((before: string[]) => {
const all = Array.from(document.querySelectorAll('#window-container [id^="window-"]')).map((e) => e.id);
return all.find((id) => !before.includes(id)) ?? all[all.length - 1] ?? null;
}, winsBefore);
expect(winId, 'the 3D viewer should open a new top-level window').toBeTruthy();
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
// Let the INITIAL render settle through the safe per-frame pump (Workers boot here).
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await page.waitForTimeout(5000); // eslint-disable-line -- skipped known-issue spec (never runs)
fix(ci): serialize the heavy 3D-viewer e2e specs (dedicated chromium-ci-3d project) The three heavy 3D-viewer specs (3d-viewer, 3d-viewer-deadlock, 3d-viewer-models) crashed on CI (run 28604015154): "Target crashed", "browser has been closed", and a black frozen canvas. Each boots the 3D-enabled pcbnew build, which pre-warms ~hardwareConcurrency*2+8 (~68 on the 30-core VM) Web Workers AND runs a multi-threaded CPU raytracer over SwiftShader software-WebGL. Under fullyParallel the three spec FILES run in separate browser processes concurrently, and the simultaneous raytrace tabs exhaust the shared GPU process's ~16 live-WebGL-context limit / renderer memory. The pool pre-warm (the deadlock fix, 7630c7e) is correct and present in the tested binary; per-file isolation prevented in-process Worker accumulation but NOT this cross-process concurrency — the gap these specs' first CI exposure hit. Fix (test-orchestration only; no wasm/submodule changes): - Route the three specs to a dedicated `chromium-ci-3d` Playwright project (own outputDir=pw-artifacts/kicad-3d so its failure traces survive the later perf run's wipe; the existing tests/pw-artifacts/** upload already captures it). - test:kicad:ci runs it as a SECOND `playwright test` pass with --workers=1, so at most one raytrace tab is alive at a time (the environment in which a single 3D load is reliably green — docs/features/3d-raytracer/README.md §7). An exit-code-aggregating sequencer runs both phases and fails if either fails (no silent skip of the 3D pass). - Cap phase-1 CI workers to 12 as headroom under the WebGL-context cap. - Log a resource-diagnostic snapshot (hardwareConcurrency, pthread pool, GL-canvas count, wasm/JS heap) before each 3D interaction so any recurrence is attributable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:49:23 +02:00
await logThreeDDiag(page, 'deadlock: after open+settle');
// Read the newest glcanvas-* (the 3D viewer) client rect in viewport coords.
const canvasRect = () => page.evaluate(() => {
const list = document.querySelectorAll('canvas[id^="glcanvas-"]');
const el = list[list.length - 1] as HTMLCanvasElement;
const r = el.getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});
// Sample the viewer canvas backing store: distinct colours (board rendered?) plus a
// coarse pixel signature (did the render change after the camera moved?).
const sampleCanvas = () => page.evaluate(() => {
const list = document.querySelectorAll('canvas[id^="glcanvas-"]');
const el = list[list.length - 1] as HTMLCanvasElement;
const tmp = document.createElement('canvas');
tmp.width = el.width; tmp.height = el.height;
fix(ci): 3D specs back to headless SwiftShader + GPU hardening; skip real-GPU drags on CI and the models render tail everywhere (product bug) The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With --use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the GPU-less CI VM: runs 28652367347 + 28664038296 both log `glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL canvas fails to create) and every failure is the FIRST viewer-open wait timing out. "Timeouts instead of crashes" meant no GL context existed to crash, not that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox project, but not for Chromium on this image. Headless SwiftShader is the environment where GL provably works — the serialized run 28649537489 opened the viewer and rendered/sampled 56 colours (viewer:26/130/209 all passed single-tab). Its remaining failures split into two families, each addressed on its own evidence: 1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame): - chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus --disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn stalls the software-GL GPU process until the watchdog shoots it (CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog off a slow op completes, and without the crash limit a lost GPU process keeps restarting for later tests. Same args locally (no-ops on a healthy real GPU). - Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas + JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock spec's 1.5s-interval settle polling. Sampled pixels and sig order are unchanged. - edge-resize spec: keep the open + 5-resize-handles assertions on CI, then test.skip the drag — each resize step re-raytraces synchronously on the wasm main thread and the 12-step drag blocked mouse.move past the 240s budget even single-tab. Real-GPU-paced; still runs locally (deadlock spec's CI-skip from d9900fe unchanged, same rationale). 2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI): raytracing a scene WITH component models kills the Chromium renderer process outright ~6s after scene build — silent process death with a FLAT wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error, no wasm abort, no crash report. Deterministic on a real GPU (5/5), fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically); the same death hits CI SwiftShader at ~66s. Board-only raytraces complete and stay alive, so the bug is specific to the model path. The spec's one historical green ended at raytrace-age ~6s — inside the death window by luck; every CI run of it ever died. Fix here: front-load the bridge assertions (ensure requests fire during scene BUILD, before the raytrace), so protocol regressions still fail the suite everywhere, and test.skip the render tail unconditionally with the bug documented at the skip site. The raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only render. Bug tracked for a dedicated fix session. Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock skip reason). Expected CI matrix: viewer open/render/stacking/titlebar + models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI by the standalone wx harnesses. Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped (the documented models render tail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:40 +02:00
// One full-frame read on a CPU-backed canvas, then sample in JS. This sampler is
// POLLED every 1.5s during an active raytrace (settleRender) — as 256 per-pixel
// getImageData GPU round-trips per poll it was the worst "GPU stall due to
// ReadPixels" offender on software WebGL (see 3d-viewer.spec.ts).
const ctx = tmp.getContext('2d', { willReadFrequently: true })!;
ctx.drawImage(el, 0, 0);
fix(ci): 3D specs back to headless SwiftShader + GPU hardening; skip real-GPU drags on CI and the models render tail everywhere (product bug) The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With --use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the GPU-less CI VM: runs 28652367347 + 28664038296 both log `glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL canvas fails to create) and every failure is the FIRST viewer-open wait timing out. "Timeouts instead of crashes" meant no GL context existed to crash, not that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox project, but not for Chromium on this image. Headless SwiftShader is the environment where GL provably works — the serialized run 28649537489 opened the viewer and rendered/sampled 56 colours (viewer:26/130/209 all passed single-tab). Its remaining failures split into two families, each addressed on its own evidence: 1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame): - chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus --disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn stalls the software-GL GPU process until the watchdog shoots it (CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog off a slow op completes, and without the crash limit a lost GPU process keeps restarting for later tests. Same args locally (no-ops on a healthy real GPU). - Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas + JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock spec's 1.5s-interval settle polling. Sampled pixels and sig order are unchanged. - edge-resize spec: keep the open + 5-resize-handles assertions on CI, then test.skip the drag — each resize step re-raytraces synchronously on the wasm main thread and the 12-step drag blocked mouse.move past the 240s budget even single-tab. Real-GPU-paced; still runs locally (deadlock spec's CI-skip from d9900fe unchanged, same rationale). 2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI): raytracing a scene WITH component models kills the Chromium renderer process outright ~6s after scene build — silent process death with a FLAT wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error, no wasm abort, no crash report. Deterministic on a real GPU (5/5), fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically); the same death hits CI SwiftShader at ~66s. Board-only raytraces complete and stay alive, so the bug is specific to the model path. The spec's one historical green ended at raytrace-age ~6s — inside the death window by luck; every CI run of it ever died. Fix here: front-load the bridge assertions (ensure requests fire during scene BUILD, before the raytrace), so protocol regressions still fail the suite everywhere, and test.skip the render tail unconditionally with the bug documented at the skip site. The raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only render. Bug tracked for a dedicated fix session. Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock skip reason). Expected CI matrix: viewer open/render/stacking/titlebar + models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI by the standalone wx harnesses. Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped (the documented models render tail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:40 +02:00
const img = ctx.getImageData(0, 0, el.width, el.height).data;
const colors = new Set<string>();
let sig = '';
for (let i = 0; i < 16; i++) {
for (let j = 0; j < 16; j++) {
fix(ci): 3D specs back to headless SwiftShader + GPU hardening; skip real-GPU drags on CI and the models render tail everywhere (product bug) The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With --use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the GPU-less CI VM: runs 28652367347 + 28664038296 both log `glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL canvas fails to create) and every failure is the FIRST viewer-open wait timing out. "Timeouts instead of crashes" meant no GL context existed to crash, not that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox project, but not for Chromium on this image. Headless SwiftShader is the environment where GL provably works — the serialized run 28649537489 opened the viewer and rendered/sampled 56 colours (viewer:26/130/209 all passed single-tab). Its remaining failures split into two families, each addressed on its own evidence: 1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame): - chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus --disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn stalls the software-GL GPU process until the watchdog shoots it (CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog off a slow op completes, and without the crash limit a lost GPU process keeps restarting for later tests. Same args locally (no-ops on a healthy real GPU). - Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas + JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock spec's 1.5s-interval settle polling. Sampled pixels and sig order are unchanged. - edge-resize spec: keep the open + 5-resize-handles assertions on CI, then test.skip the drag — each resize step re-raytraces synchronously on the wasm main thread and the 12-step drag blocked mouse.move past the 240s budget even single-tab. Real-GPU-paced; still runs locally (deadlock spec's CI-skip from d9900fe unchanged, same rationale). 2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI): raytracing a scene WITH component models kills the Chromium renderer process outright ~6s after scene build — silent process death with a FLAT wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error, no wasm abort, no crash report. Deterministic on a real GPU (5/5), fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically); the same death hits CI SwiftShader at ~66s. Board-only raytraces complete and stay alive, so the bug is specific to the model path. The spec's one historical green ended at raytrace-age ~6s — inside the death window by luck; every CI run of it ever died. Fix here: front-load the bridge assertions (ensure requests fire during scene BUILD, before the raytrace), so protocol regressions still fail the suite everywhere, and test.skip the render tail unconditionally with the bug documented at the skip site. The raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only render. Bug tracked for a dedicated fix session. Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock skip reason). Expected CI matrix: viewer open/render/stacking/titlebar + models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI by the standalone wx harnesses. Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped (the documented models render tail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:40 +02:00
const p = (Math.floor(el.height * j / 16) * el.width
+ Math.floor(el.width * i / 16)) * 4;
colors.add(`${img[p]},${img[p + 1]},${img[p + 2]}`);
sig += `${img[p]}.${img[p + 1]}.${img[p + 2]}|`;
}
}
return { distinctColors: colors.size, sig };
});
// Main-thread liveness probe. A pthread-join deadlock hangs the wasm main thread → the
// browser main JS thread is blocked → in-page polling can't run → this times out.
// Returns false on a freeze rather than throwing.
const mainThreadAlive = () => page.waitForFunction(() => {
const r = (window as unknown as { wxElementRegistry?: { findAll: (o: unknown) => unknown[] } })
.wxElementRegistry;
return !!r && r.findAll({ visible: true }).length > 0;
}, null, { timeout: 15000 }).then(() => true).catch(() => false);
const abortLines = () => [...testLogger.consoleLogs, ...testLogger.errors].filter((l) =>
l.includes('Aborted(')
|| l.toLowerCase().includes('invalid state')
|| l.toLowerCase().includes('uncaught exception: unwind')
|| l.toLowerCase().includes('indirect call to null'));
// Run an interaction with a hard wall-clock bound: a hard freeze can also hang the CDP
// input dispatch, so this fails fast instead of at the 240s test timeout.
const bounded = async (fn: () => Promise<void>, ms: number): Promise<boolean> => {
let froze = false;
await Promise.race([
fn().catch(() => { /* an in-wasm abort surfaces via abortLines(), not here */ }),
new Promise<void>((r) => setTimeout(() => { froze = true; r(); }, ms)),
]);
return froze;
};
// Rotate the camera: left-drag inside the GL region (below the ~28px titlebar).
const rotate = async () => {
const c = await canvasRect();
const cx = c.x + c.w / 2;
const cy = c.y + c.h / 2 + 40;
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 140, cy + 70, { steps: 14 });
await page.mouse.move(cx - 90, cy + 20, { steps: 10 });
await page.mouse.up();
};
// Wait for the raytrace kicked by a camera move to CONVERGE (canvas signature stable
// across two samples) before the next move, so successive renders don't overlap and
// momentarily demand more pthread Workers than the pool holds. On the fixed build this
// returns in a few polls; a mid-render deadlock is caught by the assertLive that follows.
const settleRender = async (maxMs: number) => {
let prev = '';
const start = Date.now();
while (Date.now() - start < maxMs) {
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await page.waitForTimeout(1500); // eslint-disable-line -- skipped known-issue spec (never runs)
const s = (await sampleCanvas()).sig;
if (s === prev) return;
prev = s;
}
};
// After each step: nothing aborted, main thread still live.
const assertLive = async (step: string) => {
expect(abortLines(), `raytrace aborted during "${step}":\n${abortLines().join('\n\n')}`)
.toEqual([]);
expect(await mainThreadAlive(),
`wasm main thread unresponsive after "${step}" → deadlock`).toBe(true);
};
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
// Since the 3D-webgl port (kicad eb13ff3bdc) the wasm viewer defaults to the REAL
// OpenGL renderer; the deadlock mechanism this spec guards is raytracer-specific, so
// flip the viewer to the raytracer via its toolbar toggle. Loud failure if the
// toggle moved — a silent no-op would leave the fast GL renderer making every
// liveness assertion below vacuously green.
const sigOnGl = (await sampleCanvas()).sig;
const toggled = (await clickToolbarTool(page, 'Use raytracing'))
|| (await clickByTooltip(page, 'Render current view using Raytracing'));
expect(toggled, 'the "Use raytracing" toolbar toggle must exist in the 3D viewer').toBe(true);
// Engine cross-check (guards the same false green): with NO input between the two
// samples, only an engine change repaints the canvas differently — the raytraced
// frame is lit/shadowed differently from the GL frame it replaces, a GL re-render
// reproduces the identical pixels, and a no-op leaves the canvas untouched. (Heap
// growth is NOT a usable signal here: mimalloc satisfies the raytracer's buffers
// from already-freed arena pages, so HEAPU8.length stays flat.)
let raytracerEngaged = false;
for (let i = 0; i < 20 && !raytracerEngaged; i++) {
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await page.waitForTimeout(1000); // eslint-disable-line -- skipped known-issue spec (never runs)
fix(e2e): rescope the 3D fixes for the webgl-era viewer (rebased onto main) The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the occ_service worker) made the raytracer-era orchestration on this branch moot — main's chromium-ci phase is green at 15-way parallelism (28666407570 / 28698861536). Drop what no longer earns its complexity, keep the diagnostics, fix main's live flake, and make the deadlock spec test what it was written for. - REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the SwiftShader GPU-process flags, and the resize-drag/models skips: config and package.json are byte-for-byte back to main's shape. The raytracer contention they guarded is no longer on the CI path. - FIX main's live flake: run 28698861536 is green only via retry (3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO pixel signature — the viewer's first frame lags the canvas's creation on software WebGL under parallel load, and sampling too early reads an all-black backbuffer. New waitForThreeDRender() gates render assertions on actual pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by 3d-viewer.spec:26 and the models render tail. - KEEP the storm-proofed samplers (one full-frame getImageData on a willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample — the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag instrumentation: engine-independent, and they de-risk every remaining software-GL pixel read. - models spec: bridge assertions stay front-loaded (the protocol regression signal is independent of the render); the occ_service parse verdict is now POLLED — it lands async relative to the bridge ensures, so asserting it immediately raced the worker; the render tail runs again everywhere. (The pre-webgl raytracer+models renderer-death documented in a17f3be does not affect the OpenGL default path — the raytracer-toggle+models combination remains untested product surface, tracked outside this branch.) - deadlock spec: the deadlock it guards is raytracer-specific and the viewer now defaults to OpenGL — on the GL engine it either passes vacuously (fast renders make every liveness assertion trivial, 28698861536) or fails on the black first frame (28666407570). It now flips the engine via the "Use raytracing" toolbar toggle (loud assert if the toggle moved) and cross-checks engagement by requiring the canvas pixels to CHANGE after the flip with no input in between (the raytraced frame is lit differently; a GL re-render reproduces identical pixels; heap growth is unusable — mimalloc satisfies the raytracer from freed arena pages). That guard immediately caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the click lands and "Reload time" updates, but the canvas never changes — suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg while RenderEngineChanged() reads GetAppSettings<…>(), possibly different instances in the merged bundle). The spec is therefore test.skip-annotated as a KNOWN ISSUE with the full engine-force machinery in place — unskipping it self-validates the product fix. The CI-skip also stays (raytracer liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is covered on CI by the standalone wx harnesses). - 180s viewer-open waits kept as pure CI headroom (never slow a passing run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:37:27 +02:00
raytracerEngaged = (await sampleCanvas()).sig !== sigOnGl;
}
expect(raytracerEngaged,
'the canvas did not change after the engine toggle — the raytracer did not engage')
.toBe(true);
// Let the first full raytrace converge before taking the interaction baseline.
await settleRender(25000);
const before = await sampleCanvas();
console.log(`[TEST] 3D render before interaction: ${before.distinctColors} distinct colours`);
// THE deadlock path: a left-drag on the 3D canvas rotates the model; the terminating
// mouse button events drive wxApp::HandleMouseEvent's synchronous Paint() of the
// wxGLCanvas, running the multi-threaded CPU raytracer. Pre-fix its on-demand pthread
// Worker boot deadlocks the main thread; post-fix the pre-warmed pool covers it and each
// move stays live. Two moves with a settle between mirror the user's "move the model …
// move it again".
let froze = await bounded(rotate, 30000);
expect(froze, 'the first camera-rotate drag froze the wasm main thread (deadlock)').toBe(false);
await assertLive('camera rotate');
await settleRender(25000);
await assertLive('camera rotate settle');
// Validity: the render changed → the synthetic mouse actually reached the 3D canvas
// (guards against a false green where the drag missed the canvas entirely).
const mid = await sampleCanvas();
expect(mid.sig,
'the 3D render did not change after the first camera move — the synthetic mouse '
+ 'likely never reached the 3D canvas (invalid repro), or the render stalled')
.not.toBe(before.sig);
froze = await bounded(rotate, 30000);
expect(froze, 'the second camera-rotate drag froze the main thread').toBe(false);
await assertLive('camera rotate again');
await settleRender(25000);
await assertLive('camera rotate again settle');
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate Squash of experiment/ff-big-modules vs main. Big-module routing removed: native-EH shrank kicad_editor below SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT crutch are gone — kicad-firefox and kicad-chromium both run the full suite, with the module compiled the way real users' browsers compile it. Per-engine screenshots end to end: stableShot/shotPath write test-results/<engine>/<name>.png; baselines move to baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on <engine>/<name>. Previously Firefox and Chromium renders of one spec overwrote each other and Firefox renders were never actually gated. Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium web-suite shots); manifest generated from the baseline tree. One merged playwright.config.ts (kicad/asyncify/coroutine/perf as projects); ~25 dead npm scripts dropped. The web suite is gated in CI for the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after npm ci; last 26 blind-sleep violations fixed. SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe (--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader transiently failed the first post-board-load draw and the recovery cascade ended in a silent permanent Cairo fallback — that engine flip was the "~1.2% changedRatio both directions" occ-export baseline flake. Validated 160/160 across two 80-repeat rigs; full analysis in docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium baselines shift slightly on llvmpipe — promote once from the first green run. Deflakes the new coverage exposed: presence baselines settle before capture; presence fixtures declare current file formats; perf gets its own outputDir so CI evidence survives; occ-export settles the board paint before the export dialog; menu-item waits (waitForRenderedByLabel before clickMenuItem) in 4 specs + the TESTING.md rule. Web suite runs the PROD build, in parallel: webServer becomes backend `start` + the standalone's e2e:preview (build-preview.mjs: link-wasm → stash the public/wasm symlink aside during vite build, build-demo.mjs's move — then vite preview as the persistent server). The wasm middleware serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread worker script's own response must carry COEP or Chrome kills it with ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time; VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed: shared-page specs become serial groups; locks.spec grabs alice's exact item via the new kicadCollabTestSelectByUuid hook (cross-tab "first footprint" order is not a ysync invariant); quit specs poll page.url() (quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox). Suite: 51 passed / 12 skipped / 0 failed in 1.6m. CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be reachable from the npm scripts the workflows invoke — scraped from .github/workflows/, resolved through package.json, coverage asked from playwright --list itself. Rules: uncovered-spec + orphan-project (with a documented LOCAL_ONLY_PROJECTS allowlist). Gating next to lint:determinism; 138 spec files / 13 projects accounted for. Product fixes kept from the investigations (reachable on real GPUs too): wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush + per-instance font texture + first-frame GL-error drain (GAL recovery recovers instead of falling back to Cairo) + the user-facing eeschema switch navigates again under __EMSCRIPTEN__ (project-sync's FaceRegistered gate had rerouted it into the hidden sync player; caught by the newly-gated web suite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
await page.screenshot({ path: shotPath(page, '3d-viewer-deadlock.png'), scale: 'device' });
// Sanity: the board still renders (not blank / crashed) after both moves.
fix(ci): serialize the heavy 3D-viewer e2e specs (dedicated chromium-ci-3d project) The three heavy 3D-viewer specs (3d-viewer, 3d-viewer-deadlock, 3d-viewer-models) crashed on CI (run 28604015154): "Target crashed", "browser has been closed", and a black frozen canvas. Each boots the 3D-enabled pcbnew build, which pre-warms ~hardwareConcurrency*2+8 (~68 on the 30-core VM) Web Workers AND runs a multi-threaded CPU raytracer over SwiftShader software-WebGL. Under fullyParallel the three spec FILES run in separate browser processes concurrently, and the simultaneous raytrace tabs exhaust the shared GPU process's ~16 live-WebGL-context limit / renderer memory. The pool pre-warm (the deadlock fix, 7630c7e) is correct and present in the tested binary; per-file isolation prevented in-process Worker accumulation but NOT this cross-process concurrency — the gap these specs' first CI exposure hit. Fix (test-orchestration only; no wasm/submodule changes): - Route the three specs to a dedicated `chromium-ci-3d` Playwright project (own outputDir=pw-artifacts/kicad-3d so its failure traces survive the later perf run's wipe; the existing tests/pw-artifacts/** upload already captures it). - test:kicad:ci runs it as a SECOND `playwright test` pass with --workers=1, so at most one raytrace tab is alive at a time (the environment in which a single 3D load is reliably green — docs/features/3d-raytracer/README.md §7). An exit-code-aggregating sequencer runs both phases and fails if either fails (no silent skip of the 3D pass). - Cap phase-1 CI workers to 12 as headroom under the WebGL-context cap. - Log a resource-diagnostic snapshot (hardwareConcurrency, pthread pool, GL-canvas count, wasm/JS heap) before each 3D interaction so any recurrence is attributable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:49:23 +02:00
await logThreeDDiag(page, 'deadlock: after moves');
const after = await sampleCanvas();
console.log(`[TEST] 3D render after interaction: ${after.distinctColors} distinct colours`);
expect(after.distinctColors,
'the 3D viewer should still render the board (many colours) after the camera moves')
.toBeGreaterThan(8);
});
});