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.
|
// eeschema sheet rebinds — the bridge re-reads it on every startPresence.
|
||||||
const crossAppRef = React.useRef<CrossAppHandle | null>(null);
|
const crossAppRef = React.useRef<CrossAppHandle | null>(null);
|
||||||
const siblingRestageRef = React.useRef<SiblingRestageHandle | 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);
|
const sheetManagerRef = React.useRef<SheetCollabManager | null>(null);
|
||||||
// The single-room collab doc (pcbnew/pl_editor), for the layout save-sync
|
// The single-room collab doc (pcbnew/pl_editor), for the layout save-sync
|
||||||
// (miss 08B); eeschema routes per sheet through the manager instead.
|
// (miss 08B); eeschema routes per sheet through the manager instead.
|
||||||
|
|
@ -1328,6 +1331,9 @@ export function WasmTool({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fresh mount (or StrictMode re-run): re-arm the deferred starters.
|
||||||
|
disposedRef.current = false;
|
||||||
|
|
||||||
const win = window as ToolWindow;
|
const win = window as ToolWindow;
|
||||||
|
|
||||||
// OOM recovery (feature 0002): watch for soft aborts + a stale hard-kill
|
// 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
|
// 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
|
// one-shot boot snapshot. Same opt-out as the room collab; read-only
|
||||||
// viewers skip it (they can't run the sync anyway).
|
// 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) {
|
if (tool === "pcbnew" && collabHandle && !readOnly) {
|
||||||
siblingRestageRef.current = await startSiblingRestage({
|
const startRestageIdle = () => {
|
||||||
win,
|
if (disposedRef.current) return;
|
||||||
slug,
|
void startSiblingRestage({
|
||||||
scopeId,
|
win,
|
||||||
projectId,
|
slug,
|
||||||
files,
|
scopeId,
|
||||||
targetPath,
|
projectId,
|
||||||
// Presence-scoped: connect a sheet's room only while a peer
|
files,
|
||||||
// announces it open (zero sibling sockets when alone). Absent
|
targetPath,
|
||||||
// (provider "none" / connect failed) ⇒ eager fallback.
|
// Presence-scoped: connect a sheet's room only while a peer
|
||||||
presence: crossAppRef.current ?? undefined,
|
// announces it open (zero sibling sockets when alone). Absent
|
||||||
provider: yjsProviderConfig(),
|
// (provider "none" / connect failed) ⇒ eager fallback.
|
||||||
log: append,
|
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) {
|
if (collabHandle && targetPath && COLLAB_TOOLS.has(tool) && !readOnly) {
|
||||||
driftRef.current = startDriftDetection({
|
driftRef.current = startDriftDetection({
|
||||||
|
|
@ -1921,6 +1957,9 @@ export function WasmTool({
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
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
|
// A consent dialog pending at unmount resolves false — the boot IIFE
|
||||||
// bails without ever starting the download.
|
// bails without ever starting the download.
|
||||||
consentResolveRef.current?.(false);
|
consentResolveRef.current?.(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue