test: 💍 deadlock test + readme update

This commit is contained in:
Istvan Matejcsok 2026-06-18 16:48:00 +02:00
commit 5d18bd0c45
4 changed files with 235 additions and 81 deletions

View file

@ -1,19 +1,26 @@
import { test, expect } from './utils/fixtures';
// Reproduction + fix validation for the KiCad WASM raytracer threading deadlock
// Proves the KiCad WASM raytracer's threading mechanisms genuinely run MULTI-CORE
// (kicad/3d-viewer/3d_rendering/raytracing/render_3d_raytrace_base.cpp). One binary,
// switchable by URL ?m=:
// m=0 A detached threads + std::this_thread::sleep_for -> DEADLOCK (reproduces the bug)
// m=0 A detached threads + main-thread busy-wait -> DEADLOCK (negative control)
// m=1 B1 detached threads + emscripten_sleep yield -> multi-core (works in isolation)
// m=2 B2 persistent pool + emscripten_sleep yield -> multi-core (works in isolation)
// m=3 C serial on the calling thread -> the shipped fallback
// m=3 C serial on the calling thread -> baseline for the speedup test only
// m=4 B1 with stack-local atomics -> multi-core (locals survive yield)
// m=5 B3 persistent pool + sleep_for busy-wait -> multi-core (no emscripten_sleep)
//
// POLARITY: each test is green IFF the mechanism actually parallelizes (workersRan > 1)
// and red otherwise. The naive busy-wait (m=0) is kept ONLY as a negative control: it is
// held to the SAME bar (workersRan > 1), cannot meet it (it deadlocks), and is marked
// `test.fail()` so it shows as an EXPECTED failure — never a green pass. A frozen tab is
// never a passing state. (Full deadlock finding: docs/features/async/11-asyncify-nesting-raytracer.md.)
// The serial mode (m=3) is exercised only as the slower baseline inside the speedup test,
// not as a standalone pass (green-on-single-core would be backwards).
//
// NOTE: this is a STANDALONE wxWidgets harness — it re-implements the threading patterns
// itself and has NO connection to KiCad's render_3d_raytrace_base.cpp. It passes
// regardless of what the real viewer ships; it exists to prove the deadlock + the fix
// mechanism in seconds (not the ~12-min KiCad build).
// itself and has NO connection to KiCad's render_3d_raytrace_base.cpp. It validates the
// threading MECHANISM in seconds (not the ~12-min KiCad build), not the shipped viewer.
//
// TODO(asyncify-nesting): the real KiCad 3D viewer currently ships SERIAL (single-core).
// The emscripten_sleep variants (B1/B2/m4) pass HERE but ABORT the real viewer with
@ -51,21 +58,13 @@ async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string
.toBe( true );
}
test.describe( 'Raytracer threading (render_3d_raytrace_base.cpp repro)', () => {
test( 'C: serial baseline completes on a single thread', async ( { page, testLogger } ) => {
await page.goto( `${APP}#m=3&${WORK}` );
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=3' );
const r = parseSuccess( testLogger.consoleLogs, 3 )!;
expect( r.workersRan, 'serial runs on exactly one thread' ).toBe( 1 );
} );
test.describe( 'Raytracer threading (render_3d_raytrace_base.cpp) — must run multi-core', () => {
test( 'B1: detached threads + emscripten_sleep yield → multi-core, no deadlock', async ( { page, testLogger } ) => {
await page.goto( `${APP}#m=1&${WORK}` );
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=1' );
const r = parseSuccess( testLogger.consoleLogs, 1 )!;
expect( r.workersRan, 'work should run on multiple worker threads' ).toBeGreaterThan( 1 );
expect( testLogger.consoleLogs.some( l => l.includes( '[RTPOOL] FAIL' ) ) ).toBe( false );
} );
test( 'B2: persistent pool + yield → multi-core across repeated passes', async ( { page, testLogger } ) => {
@ -86,7 +85,6 @@ test.describe( 'Raytracer threading (render_3d_raytrace_base.cpp repro)', () =>
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=4' );
const r = parseSuccess( testLogger.consoleLogs, 4 )!;
expect( r.workersRan, 'work runs on multiple threads with local atomics' ).toBeGreaterThan( 1 );
expect( testLogger.consoleLogs.some( l => l.includes( '[RTPOOL] FAIL' ) ) ).toBe( false );
} );
test( 'B3: persistent pool + sleep_for busy-wait (real raytracer mechanism) → multi-core', async ( { page, testLogger } ) => {
@ -97,7 +95,6 @@ test.describe( 'Raytracer threading (render_3d_raytrace_base.cpp repro)', () =>
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=5' );
const r = parseSuccess( testLogger.consoleLogs, 5 )!;
expect( r.workersRan, 'busy-wait pool runs on multiple cores' ).toBeGreaterThan( 1 );
expect( testLogger.consoleLogs.some( l => l.includes( '[RTPOOL] FAIL' ) ) ).toBe( false );
} );
test( 'multi-core (B1) is faster than serial (C)', async ( { page, testLogger } ) => {
@ -115,12 +112,17 @@ test.describe( 'Raytracer threading (render_3d_raytrace_base.cpp repro)', () =>
expect( parallel.totalMs, 'parallel should beat serial' ).toBeLessThan( serial.totalMs );
} );
test( 'A: detached threads + busy-wait reproduces the deadlock', async ( { page, testLogger } ) => {
// The main thread busy-waits, never returns to the event loop, so the on-demand
// workers never start. The in-test 12s guard then prints the DEADLOCK marker.
// Negative control. The naive desktop pattern (detached threads + a main-thread
// busy-wait) DEADLOCKS in the browser: the busy-wait starves the event loop, so the
// on-demand workers never spawn and workersRan stays 0. Marked test.fail(): it is held
// to the SAME bar as every other test (workersRan > 1) and is EXPECTED to miss it, so
// it is reported as an expected failure — never a green pass. If this ever PASSES, the
// deadlock got solved and this should graduate into a real test.
test( 'A (negative control): naive detached threads + busy-wait CANNOT run multi-core', async ( { page, testLogger } ) => {
test.fail();
await page.goto( `${APP}#m=0&${WORK}`, { waitUntil: 'domcontentloaded' } );
await waitForLog( testLogger, '[RTPOOL] DEADLOCK', 30000 );
expect( testLogger.consoleLogs.some( l => l.includes( '[RTPOOL] SUCCESS' ) ),
'no pass should succeed under the busy-wait' ).toBe( false );
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=0', 20000 );
const r = parseSuccess( testLogger.consoleLogs, 0 )!;
expect( r.workersRan, 'the naive busy-wait cannot parallelize in the browser' ).toBeGreaterThan( 1 );
} );
} );