diagnostics: configurable --diag logging flags + asyncify setupUIConditions fix
- build-pcbnew.sh: add --diag=<gal,coroutine,ctor,all> -> -DKICAD_DIAG_*, off by default (forwarded by docker/build.sh) - diagnostics.js: emit at console.log level (no longer error/warn); still gated by SHIM_DIAGNOSTICS=1 - apply-asyncify.sh: exclude PCB_EDIT_FRAME::setupUIConditions() from asyncify instrumentation (V8 cannot run the instrumented huge function on the rewound ctor stack -> Chrome startup stall; Firefox unaffected) - DEBUG.md: reusable WASM/asyncify/browser debugging guide, diagnostic flag docs, and a production-build (release + -O2 asyncify) recipe - tests: standalone coroutine vcall/gl repro probes - bump kicad + wxwidgets submodules (diagnostic gating / debug cleanup) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e4cf5fb461
commit
7331619404
10 changed files with 768 additions and 18 deletions
|
|
@ -650,3 +650,45 @@ $(S)/coroutine-pthread/mainloop_repro.html: $(S)/coroutine-pthread/mainloop_repr
|
|||
|
||||
coroutine-pthread-mainloop: $(S)/coroutine-pthread/mainloop_repro.html
|
||||
.PHONY: coroutine-pthread-mainloop
|
||||
|
||||
# WebGL2 + coroutine reproduction (no wx, no pthreads, default shell with #canvas)
|
||||
LDFLAGS_COROUTINE_GL = $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sASYNCIFY_IMPORTS=['emscripten_fiber_swap'] \
|
||||
-sDYNCALLS=1 -sMAX_WEBGL_VERSION=2 -sMIN_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall']
|
||||
|
||||
$(S)/coroutine-pthread/gl_repro.o: $(S)/coroutine-pthread/gl_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/gl_repro.html: $(S)/coroutine-pthread/gl_repro.o $(S)/coroutine/libcontext.o
|
||||
$(CXX) $^ $(LDFLAGS_COROUTINE_GL) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
coroutine-pthread-gl: $(S)/coroutine-pthread/gl_repro.html
|
||||
.PHONY: coroutine-pthread-gl
|
||||
|
||||
# WebGL2 + coroutine + PTHREADS (the last untested combo: KiCad uses GL + pthreads together)
|
||||
LDFLAGS_COROUTINE_GL_PTHREAD = $(LDFLAGS_COROUTINE_GL) -pthread \
|
||||
-sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0
|
||||
|
||||
$(S)/coroutine-pthread/gl_repro_pt.o: $(S)/coroutine-pthread/gl_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/gl_repro_pt.html: $(S)/coroutine-pthread/gl_repro_pt.o $(S)/coroutine-pthread/libcontext_pt.o
|
||||
$(CXX) $^ $(LDFLAGS_COROUTINE_GL_PTHREAD) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
coroutine-pthread-gl-pt: $(S)/coroutine-pthread/gl_repro_pt.html
|
||||
.PHONY: coroutine-pthread-gl-pt
|
||||
|
||||
$(S)/coroutine-pthread/vcall_repro.o: $(S)/coroutine-pthread/vcall_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/vcall_repro.html: $(S)/coroutine-pthread/vcall_repro.o $(S)/coroutine-pthread/libcontext_pt.o
|
||||
$(CXX) $^ $(LDFLAGS_COROUTINE_PTHREAD_NOWX) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
coroutine-pthread-vcall: $(S)/coroutine-pthread/vcall_repro.html
|
||||
.PHONY: coroutine-pthread-vcall
|
||||
|
|
|
|||
78
tests/apps/standalone/coroutine-pthread/gl_repro.cpp
Normal file
78
tests/apps/standalone/coroutine-pthread/gl_repro.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Reproduction probe #5: WebGL 2.0 + coroutine, the last untested KiCad factor.
|
||||
//
|
||||
// KiCad's GAL renders via WebGL 2.0 in the rAF refresh, and tool coroutines activate
|
||||
// during the same refresh — so the Asyncify unwind/rewind happens MID-RENDER-FRAME with
|
||||
// the GL context current. This probe creates a real WebGL-2.0 context and activates the
|
||||
// coroutine between GL draw calls inside an emscripten_set_main_loop(rAF) frame, then the
|
||||
// coroutine yields back -> main rewinds the render frame.
|
||||
//
|
||||
// No-wx (single-threaded first; GL+pthreads needs OFFSCREEN proxying — add later if this
|
||||
// passes). Firefox should reach "[REPRO] DONE"; if system Chrome crashes before DONE, the
|
||||
// WebGL x coroutine-rewind interaction is the missing factor.
|
||||
|
||||
#include "kicad_coroutine_harness.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#include <GLES3/gl3.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using coroutine_test::TestCoroutine;
|
||||
|
||||
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_ctx = 0;
|
||||
static int g_frame = 0;
|
||||
|
||||
static void run_coroutine()
|
||||
{
|
||||
TestCoroutine co( []( TestCoroutine& self ) {
|
||||
std::printf( "[REPRO] coroutine body running (mid-GL-frame), about to yield\n" );
|
||||
std::fflush( stdout );
|
||||
self.Yield( 42 );
|
||||
} );
|
||||
|
||||
bool running = co.Call( 1 ); // unwinds the render frame back to dynCall_v; yields back
|
||||
std::printf( "[REPRO] after Call: running=%d lastValue=%ld\n",
|
||||
(int) running, (long) co.LastReturnValue() );
|
||||
std::fflush( stdout );
|
||||
|
||||
running = co.Resume( 2 );
|
||||
std::printf( "[REPRO] after Resume: running=%d\n", (int) running );
|
||||
std::fflush( stdout );
|
||||
}
|
||||
|
||||
static void render_frame()
|
||||
{
|
||||
++g_frame;
|
||||
glClearColor( 0.1f, 0.2f, 0.3f, 1.0f );
|
||||
glClear( GL_COLOR_BUFFER_BIT ); // a real WebGL2 draw call before the coroutine
|
||||
|
||||
if( g_frame >= 2 )
|
||||
{
|
||||
std::printf( "[REPRO] frame %d: activating coroutine mid-GL-frame\n", g_frame );
|
||||
std::fflush( stdout );
|
||||
|
||||
run_coroutine(); // coroutine yields -> Asyncify rewinds the render frame
|
||||
|
||||
glClearColor( 0.3f, 0.2f, 0.1f, 1.0f );
|
||||
glClear( GL_COLOR_BUFFER_BIT ); // another GL call after the coroutine resumes
|
||||
std::printf( "[REPRO] DONE\n" );
|
||||
std::fflush( stdout );
|
||||
emscripten_cancel_main_loop();
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
EmscriptenWebGLContextAttributes attrs;
|
||||
emscripten_webgl_init_context_attributes( &attrs );
|
||||
attrs.majorVersion = 2;
|
||||
attrs.minorVersion = 0;
|
||||
g_ctx = emscripten_webgl_create_context( "#canvas", &attrs );
|
||||
emscripten_webgl_make_context_current( g_ctx );
|
||||
std::printf( "[REPRO] start; WebGL2 context=%d\n", (int) g_ctx );
|
||||
std::fflush( stdout );
|
||||
|
||||
emscripten_set_main_loop( render_frame, 0, 0 );
|
||||
return 0;
|
||||
}
|
||||
135
tests/apps/standalone/coroutine-pthread/vcall_repro.cpp
Normal file
135
tests/apps/standalone/coroutine-pthread/vcall_repro.cpp
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Reproduction probe #N for the KiCad Asyncify-fiber Chrome crash.
|
||||
//
|
||||
// Root-cause finding (DEBUG.md): the crash is the PCB_EDIT_FRAME ctor calling the
|
||||
// VIRTUAL setupUIConditions() *after* the first tool coroutine (InvokeTool) has
|
||||
// unwound+rewound the (deep) ctor stack via asyncify. That virtual call dispatches
|
||||
// indirectly (-fexceptions) as: wasm -> invoke_vi(JS) -> instrumented dynCall_vi(JS)
|
||||
// -> setupUIConditions. Chrome's V8 hard-crashes on it; Firefox tolerates it.
|
||||
//
|
||||
// nested_repro.cpp recreated the nested invoke_/dynCall chain + a coroutine yield and
|
||||
// PASSED in both browsers. The factor it did NOT have: a NEW indirect "vi" call made
|
||||
// from the rewound frame AFTER the coroutine round-trip. This probe adds exactly that.
|
||||
//
|
||||
// Shape (mirrors KiCad):
|
||||
// main -> level(N) ... -> level(0) (deep stack via invoke_ try-hops)
|
||||
// -> run_coroutine(): co.Call -> Yield -> co.Resume (asyncify unwind+rewind of main)
|
||||
// -> THEN g_obj->setupConditions() (virtual, in try => invoke_vi -> dynCall_vi)
|
||||
//
|
||||
// Built no-wx + pthreads + -fexceptions + DYNCALLS + asyncify + the dyncall shim
|
||||
// (LDFLAGS_COROUTINE_PTHREAD_NOWX). Firefox should reach "[REPRO] DONE"; if system
|
||||
// Chrome crashes before DONE, we've reproduced the crash in isolation.
|
||||
|
||||
#include "kicad_coroutine_harness.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
using coroutine_test::TestCoroutine;
|
||||
|
||||
static const int kBoundaries = 20; // nested invoke_/dynCall JS<->wasm hops (KiCad had ~11)
|
||||
|
||||
typedef void ( *LevelFn )( int );
|
||||
static LevelFn g_level = nullptr;
|
||||
|
||||
// Polymorphic hierarchy so the post-coroutine call is a genuine (non-devirtualizable)
|
||||
// virtual dispatch => call_indirect signature "vi" (the `this` pointer) => invoke_vi ->
|
||||
// dynCall_vi, exactly like PCB_EDIT_FRAME's virtual setupUIConditions().
|
||||
struct Base
|
||||
{
|
||||
virtual void setupConditions() { std::printf( "[REPRO] Base::setupConditions\n" ); }
|
||||
virtual ~Base() {}
|
||||
};
|
||||
struct Derived : Base
|
||||
{
|
||||
int m_n = 0;
|
||||
void setupConditions() override
|
||||
{
|
||||
// Mimic setupUIConditions: a biggish body with calls + allocations.
|
||||
volatile int s = 0;
|
||||
for( int i = 0; i < 64; ++i )
|
||||
s += i;
|
||||
m_n = s;
|
||||
std::printf( "[REPRO] Derived::setupConditions ran (n=%d)\n", m_n );
|
||||
std::fflush( stdout );
|
||||
}
|
||||
};
|
||||
|
||||
// noinline factory returning a base pointer of a runtime-chosen type so the compiler
|
||||
// cannot devirtualize the later g_obj->setupConditions() call.
|
||||
static Base* makeObj( int seed ) __attribute__( ( noinline ) );
|
||||
static Base* makeObj( int seed )
|
||||
{
|
||||
return ( seed & 1 ) ? static_cast<Base*>( new Derived() ) : new Base();
|
||||
}
|
||||
static Base* g_obj = nullptr;
|
||||
|
||||
static void run_coroutine()
|
||||
{
|
||||
// Mirror KiCad's TOOL_MANAGER pattern: RunMainStack (ContinueAfterRoot bounce) + Yield.
|
||||
TestCoroutine co( []( TestCoroutine& self ) {
|
||||
self.RunMainStack( []() {} );
|
||||
self.Yield( 42 );
|
||||
} );
|
||||
|
||||
bool running = co.Call( 1 ); // drives the bounce + unwinds main through the invoke_ chain
|
||||
running = co.Resume( 2 ); // rewinds main + resumes
|
||||
std::printf( "[REPRO] coroutine done running=%d\n", (int) running );
|
||||
std::fflush( stdout );
|
||||
|
||||
// *** THE CRASH FACTOR ***
|
||||
// Now (main stack just unwound+rewound) make a VIRTUAL call via invoke_vi ->
|
||||
// instrumented dynCall_vi from this rewound frame — exactly what the PCB_EDIT_FRAME
|
||||
// ctor does when it calls the virtual setupUIConditions() after InvokeTool.
|
||||
try
|
||||
{
|
||||
g_obj->setupConditions();
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
}
|
||||
std::printf( "[REPRO] post-coroutine virtual call returned OK\n" );
|
||||
std::fflush( stdout );
|
||||
}
|
||||
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void level( int depth )
|
||||
{
|
||||
if( depth > 0 )
|
||||
{
|
||||
// Indirect call inside a try-region => invoke_vi wrapper => one nested
|
||||
// asyncify-unwindable JS<->wasm boundary per hop (the KiCad shape).
|
||||
try
|
||||
{
|
||||
g_level( depth - 1 );
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
throw;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
run_coroutine(); // deepest hop
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
g_obj = makeObj( 1 ); // a Derived, but via a noinline factory (non-devirtualizable)
|
||||
g_level = &level;
|
||||
std::printf( "[REPRO] start, %d nested boundaries, then a virtual call after the coroutine\n",
|
||||
kBoundaries );
|
||||
std::fflush( stdout );
|
||||
|
||||
try
|
||||
{
|
||||
g_level( kBoundaries );
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
}
|
||||
|
||||
std::printf( "[REPRO] DONE\n" );
|
||||
std::fflush( stdout );
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -95,4 +95,56 @@ test.describe('Coroutine pthread main() reproduction', () => {
|
|||
'main loop should have run and activated the coroutine'
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Probe #6: WebGL 2.0 + coroutine activated mid-render-frame (KiCad's GAL render path).
|
||||
test('WebGL2 + mid-frame fiber reaches DONE without renderer crash', async ({ page, testLogger }) => {
|
||||
await page.goto('/standalone/coroutine-pthread/gl_repro.html');
|
||||
await tryLoadApp(page, 20000).catch(() => {});
|
||||
|
||||
await expect
|
||||
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
|
||||
timeout: 30000,
|
||||
message: 'should reach [REPRO] DONE (rewind of a mid-GL-frame survived)',
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
expect(
|
||||
testLogger.consoleLogs.some((l) => l.includes('WebGL2 context=')),
|
||||
'a WebGL2 context should have been created'
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Probe #7: WebGL2 + coroutine mid-frame + PTHREADS (the GL x pthreads combo KiCad uses).
|
||||
test('WebGL2 + pthreads mid-frame fiber reaches DONE without renderer crash', async ({ page, testLogger }) => {
|
||||
await page.goto('/standalone/coroutine-pthread/gl_repro_pt.html');
|
||||
await tryLoadApp(page, 25000).catch(() => {});
|
||||
|
||||
await expect
|
||||
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
|
||||
timeout: 30000,
|
||||
message: 'should reach [REPRO] DONE (GL + pthreads mid-frame rewind survived)',
|
||||
})
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
// Probe #8: a VIRTUAL call via invoke_vi -> instrumented dynCall_vi made from the
|
||||
// asyncify-rewound frame AFTER a coroutine round-trip. This is the exact factor the
|
||||
// KiCad crash has that nested_repro lacked: PCB_EDIT_FRAME's ctor calls the virtual
|
||||
// setupUIConditions() after InvokeTool's first coroutine unwinds/rewinds the ctor stack.
|
||||
test('post-coroutine virtual call (invoke_vi->dynCall_vi) reaches DONE without renderer crash', async ({ page, testLogger }) => {
|
||||
await page.goto('/standalone/coroutine-pthread/vcall_repro.html');
|
||||
await tryLoadApp(page, 25000).catch(() => {});
|
||||
|
||||
await expect
|
||||
.poll(() => testLogger.consoleLogs.some((l) => l.includes('[REPRO] DONE')), {
|
||||
timeout: 30000,
|
||||
message: 'should reach [REPRO] DONE (virtual call from the rewound frame survived)',
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
expect(
|
||||
testLogger.consoleLogs.some((l) => l.includes('post-coroutine virtual call returned OK')),
|
||||
'the post-coroutine virtual call should have completed'
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue