The ~1/9 wasm trap on CvPcb open ("index out of bounds" / "indirect call to
null" in a footprint AsyncLoad pool worker, then eeschema aborting on the
broken future — and the eeschema-fp-selector "CI-only" trap family, which was
never llvmpipe-specific) was wxString's UTF-8 build mutating SHARED strings on
read-only access from concurrent pool workers: every iterator ctor/dtor
spliced an intrusive list inside the string object, and torn splices wrote
through dead node pointers into other threads' stack frames. Second defect:
the UTF-8 position cache returned stale offsets when another thread's string
died and its address was reused.
Fixed in the wxwidgets fork (per-thread iterator registry + position cache
disabled under Emscripten) — kicad is untouched and AsyncLoad keeps its full
multi-worker fan-out. Falsified along the way (all perturbation masks, not
fixes): serializing the items, mimalloc vs dlmalloc, pthread stack size,
ASYNCIFY_STACK_SIZE, private-copy EnumFromStr, hot-path logging.
New red-first standalone app tests/apps/standalone/wxstring-mt (+ spec
coroutine-wxstring-mt.spec.ts, wx-chromium + coroutine-firefox): shared-string
compares alternating with wide-literal conversions reproduce the exact editor
trap signatures on the unfixed wx and run 4.7M rounds clean on the fixed one;
the pos-cache address-reuse dance corrupts on the first reuse before and
survives 673 after; two guard modes keep the iterator fix-up feature honest
(incl. an anti-elision liveness check — balanced register/unregister pairs in
tight loops can legally be optimized away, so a naive red test tests nothing).
Verification: wx+coroutine suites 382 passed; in-app AsyncLoad hammer 3x1000
rounds clean (baseline died <10); eeschema-assign-footprints spec 20/20
firefox + 20/20 chromium on the final build; lint:determinism clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rb9jsqtHsC3tHTaJ45244j
53 lines
2.9 KiB
TypeScript
53 lines
2.9 KiB
TypeScript
import { test, expect } from './utils/fixtures';
|
|
|
|
// wxString UTF-8 build under threads (tests/apps/standalone/wxstring-mt).
|
|
//
|
|
// The UTF-8 wxString updates bookkeeping on read-only access: an intrusive
|
|
// registry of live iterators (per-THREAD lists since the wasm-port fix in
|
|
// wxwidgets include/wx/string.h — historically a list inside each string
|
|
// object, which made concurrent reads of a SHARED string race), and a
|
|
// per-thread position cache (disabled under Emscripten — its entries could
|
|
// outlive a string destroyed by another thread and mis-describe a new string
|
|
// at a reused address). These modes were built red-first against stock wx:
|
|
// modes 1 and 4 trapped/corrupted (mode 4 with the exact editor trap
|
|
// signatures: "index out of bounds" / "indirect call to null"), and turned
|
|
// green with the per-thread registry + disabled cache. Modes 0/2/3 guard the
|
|
// surrounding behavior (mode 2/3: the iterator fix-up feature the registry
|
|
// exists for must keep working; mode 3 also proves the registration writes
|
|
// are not optimized away — without it, mode 0/4 could silently test nothing).
|
|
//
|
|
// Named coroutine-* so playwright's coroutine-firefox project runs it on real
|
|
// Firefox in addition to wx-chromium (pthread app; WebKit is skipped).
|
|
|
|
const APP = '/standalone/wxstring-mt/wxstring_mt_test.html';
|
|
|
|
const MODES: { m: number; name: string; minRounds: number }[] = [
|
|
{ m: 0, name: 'concurrent iteration of one shared string', minRounds: 10000 },
|
|
{ m: 1, name: 'position-cache cross-thread destroy + address reuse', minRounds: 10 },
|
|
{ m: 2, name: 'iterator fix-up across width-changing edits', minRounds: 1000 },
|
|
{ m: 3, name: 'registration liveness (fix-up through opaque calls)', minRounds: 100 },
|
|
{ m: 4, name: 'shared-string compares + wide-literal conversions', minRounds: 10000 },
|
|
];
|
|
|
|
async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string, timeout = 60000 ) {
|
|
await expect.poll( () => testLogger.consoleLogs.some( l => l.includes( needle ) ), { timeout } ).toBe( true );
|
|
}
|
|
|
|
test.describe( 'wxString UTF-8 multithreading (per-thread iterator registry, pos cache off)', () => {
|
|
|
|
for( const { m, name, minRounds } of MODES ) {
|
|
test( `mode ${m}: ${name}`, async ( { page, testLogger } ) => {
|
|
await page.goto( `${APP}#m=${m}` );
|
|
await waitForLog( testLogger, `[WXSTR] SUCCESS mode=${m}` );
|
|
|
|
const line = testLogger.consoleLogs.find( l => l.includes( `[WXSTR] SUCCESS mode=${m}` ) )!;
|
|
const rounds = +( line.match( /rounds=(\d+)/ )?.[1] ?? -1 );
|
|
expect( rounds, 'the mode must have done real work' ).toBeGreaterThanOrEqual( minRounds );
|
|
|
|
expect( testLogger.consoleLogs.filter( l => l.includes( '[WXSTR] CORRUPT' ) ),
|
|
'no verified corruption' ).toHaveLength( 0 );
|
|
expect( testLogger.errors.filter( e => !e.includes( 'favicon' ) ),
|
|
'no runtime errors' ).toHaveLength( 0 );
|
|
} );
|
|
}
|
|
} );
|