fix(editor): stagger the sibling restage out of the settle window
The differential-repro ladder (2026-08-02, five prod runs + local counter measurements) narrowed the crash trigger empirically: warm loads of sibling-heavy projects die at settle (V1/V4 fail 2/2 warm; V2/V3 without siblings never fail, warm or cold), while the flight-recorder counters show the settle-time collision windows themselves are universal (fcsTotal=72, rootHotTotal=3 on V1 AND V3, every load, cold and warm — so the windows are the shared fan-out, not sibling-made). The sibling restage's room connects + restage fetches are the only sibling-specific traffic contending with those windows, and warm IDB compresses it into exactly that moment. Nothing in the restage is needed for first paint — the boot snapshot staged every sibling seconds earlier — so it now starts on requestIdleCallback (5s timeout; setTimeout(3s) fallback), well clear of the settle storm. Unmount-safe via disposedRef (armed per mount, checked in the deferred starter and on handle resolution). Validation is empirical by design: the counters won't move (windows are not sibling-made); the test is warm V1/V4 prod loads no longer dying. If they still die, the sibling lever is exonerated too and the remaining suspects narrow to the ydoc-materialization path差 (second-load file source) — the next probe either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
parent
00b80a923f
commit
5c7f8a2e85
1 changed files with 53 additions and 14 deletions
|
|
@ -980,6 +980,9 @@ export function WasmTool({
|
|||
// eeschema sheet rebinds — the bridge re-reads it on every startPresence.
|
||||
const crossAppRef = React.useRef<CrossAppHandle | null>(null);
|
||||
const siblingRestageRef = React.useRef<SiblingRestageHandle | null>(null);
|
||||
// Set at the boot effect's cleanup; deferred starters bail on it (the
|
||||
// sibling-restage idle stagger can fire after unmount).
|
||||
const disposedRef = React.useRef(false);
|
||||
const sheetManagerRef = React.useRef<SheetCollabManager | null>(null);
|
||||
// The single-room collab doc (pcbnew/pl_editor), for the layout save-sync
|
||||
// (miss 08B); eeschema routes per sheet through the manager instead.
|
||||
|
|
@ -1328,6 +1331,9 @@ export function WasmTool({
|
|||
return;
|
||||
}
|
||||
|
||||
// Fresh mount (or StrictMode re-run): re-arm the deferred starters.
|
||||
disposedRef.current = false;
|
||||
|
||||
const win = window as ToolWindow;
|
||||
|
||||
// OOM recovery (feature 0002): watch for soft aborts + a stale hard-kill
|
||||
|
|
@ -1833,21 +1839,51 @@ export function WasmTool({
|
|||
// files a PCB session syncs from fresh in MEMFS, instead of the
|
||||
// one-shot boot snapshot. Same opt-out as the room collab; read-only
|
||||
// viewers skip it (they can't run the sync anyway).
|
||||
//
|
||||
// STAGGERED out of the settle window (2026-08-02, the ladder
|
||||
// result): warm sibling-heavy projects crash at exactly this moment
|
||||
// (V1/V4 fail 2/2 warm, V2/V3 without siblings never do), and the
|
||||
// asyncify flight recorder places the fatal interleave inside the
|
||||
// settle-time wake windows. The restage's room connects + restage
|
||||
// fetches were the only sibling-specific traffic contending with
|
||||
// those windows. Nothing here is needed for first paint — the boot
|
||||
// snapshot staged every sibling seconds ago — so it starts when the
|
||||
// main thread is idle (or after 5s, whichever first), well clear of
|
||||
// the settle storm. Unmount-safety: the ref may be populated after
|
||||
// unmount, so the cleanup check runs inside the callback too.
|
||||
if (tool === "pcbnew" && collabHandle && !readOnly) {
|
||||
siblingRestageRef.current = await startSiblingRestage({
|
||||
win,
|
||||
slug,
|
||||
scopeId,
|
||||
projectId,
|
||||
files,
|
||||
targetPath,
|
||||
// Presence-scoped: connect a sheet's room only while a peer
|
||||
// announces it open (zero sibling sockets when alone). Absent
|
||||
// (provider "none" / connect failed) ⇒ eager fallback.
|
||||
presence: crossAppRef.current ?? undefined,
|
||||
provider: yjsProviderConfig(),
|
||||
log: append,
|
||||
});
|
||||
const startRestageIdle = () => {
|
||||
if (disposedRef.current) return;
|
||||
void startSiblingRestage({
|
||||
win,
|
||||
slug,
|
||||
scopeId,
|
||||
projectId,
|
||||
files,
|
||||
targetPath,
|
||||
// Presence-scoped: connect a sheet's room only while a peer
|
||||
// announces it open (zero sibling sockets when alone). Absent
|
||||
// (provider "none" / connect failed) ⇒ eager fallback.
|
||||
presence: crossAppRef.current ?? undefined,
|
||||
provider: yjsProviderConfig(),
|
||||
log: append,
|
||||
}).then((handle) => {
|
||||
if (disposedRef.current) {
|
||||
handle?.destroy();
|
||||
return;
|
||||
}
|
||||
siblingRestageRef.current = handle;
|
||||
});
|
||||
};
|
||||
type IdleWindow = Window & {
|
||||
requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number;
|
||||
};
|
||||
const w = window as IdleWindow;
|
||||
if (typeof w.requestIdleCallback === "function") {
|
||||
w.requestIdleCallback(startRestageIdle, { timeout: 5000 });
|
||||
} else {
|
||||
window.setTimeout(startRestageIdle, 3000);
|
||||
}
|
||||
}
|
||||
if (collabHandle && targetPath && COLLAB_TOOLS.has(tool) && !readOnly) {
|
||||
driftRef.current = startDriftDetection({
|
||||
|
|
@ -1921,6 +1957,9 @@ export function WasmTool({
|
|||
})();
|
||||
|
||||
return () => {
|
||||
// Deferred starters (the sibling-restage idle stagger) check this
|
||||
// before creating anything after unmount.
|
||||
disposedRef.current = true;
|
||||
// A consent dialog pending at unmount resolves false — the boot IIFE
|
||||
// bails without ever starting the download.
|
||||
consentResolveRef.current?.(false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue