LearnNewsExamplesServices
Frontmatter
id16425
titleDockFlip.play wedges forever in rendering-starved documents
stateClosed
labels
bugai
assigneesneo-fable
createdAtAug 3, 2026, 1:52 AM
updatedAtAug 3, 2026, 2:44 PM
githubUrlhttps://github.com/neomjs/neo/issues/16425
authorneo-fable
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 3, 2026, 2:42 PM

DockFlip.play wedges forever in rendering-starved documents

neo-fable
neo-fable commented on Aug 3, 2026, 1:52 AM

Context

Found while closing the geometry leg of #16402. With the grid-geometry starvation carrier fixed, a dense tour in a genuinely hidden browser pane got much further than before — and then still stopped fail-closed at the canvas-update cue. This ticket was filed on the hypothesis that the Sparkline OffscreenCanvas registration handshake was the starved leg. Instrumented classification falsified that hypothesis and convicted a different owner: Neo.main.addon.DockFlip#play. This body is truth-folded to the classified mechanism; the original premise is preserved below as falsified archaeology.

Original premise (falsified — preserved as archaeology)

As filed: Workspace#pulseScaleSparkline requires a visible Sparkline with offscreenRegistered === true; in a rendering-starved document no Sparkline ever reached that state, so the fail-closed tour stopped at canvas-update. The filing hypothesized the registration handshake rode frame-scheduled choreography (main-side dispatch) or a worker-first-paint confirmation.

Falsification receipts (issuecomment-5165222999): under the full synthetic starvation rig, ALL 31 sparklines reach mounted: true, offscreenRegistered: true BEFORE any tour interaction; every transferCanvasToWorker call found its node; the triangle (afterSetMounted → worker timer → DomAccess#transferCanvasToWorkertransferControlToOffscreen → direct transfer → registerCanvasDirectcanvasRegistered ping → flag flip) is message-driven end to end — no leg rides rAF, RO, or paint. Confirmed live in a genuinely hidden pane (issuecomment-5165465808): all sparklines registered, document.hidden: true. The registration mechanism was never the starved leg — the witness spec still pins it so it cannot silently regress.

The Problem (classified)

DockFlip#play awaited raw requestAnimationFrame promises at four points — the stage-A outgoing-detach poll, the stage-B new-marker poll, the replacement settle frame, and the invert-release frame — with loops bounded in frame COUNT, not time. In a document that never services rendering opportunities, the FIRST await never resolves. Every dock re-projection funnels through refreshDockWorkspace → flip.play(), so the dense tour hung in its startTour preamble before firing a single scene event (rAF-request stack receipt names stage A). Live panes only ever progressed because CDP screenshots force compositor frames — tooling, not product.

The live-pane re-measure exposed a second starvation class: genuinely hidden pages ALSO visibility-clamp main-thread timers (>=1s/tick, ~1 wake/min intensive), so even a timer dam degrades to minutes there (witnessed: the pane preamble sat wedged across 4 polls on the dam-only iteration). Hidden documents therefore need a discriminator, not a bounded wait.

The Architectural Reality

Starvation carrier hierarchy by environment class: HIDDEN documents (document.hidden === true) — rAF dead, RO dead, main timers clamped; surviving carriers are postMessage and app-worker timers; a hidden path must not wait at all. OCCLUDED-but-visible windows — rAF dead, RO dead, main timers at normal clamps; timer dams are valid. PRESENTING documents — rAF is the fastest correct signal. Same law as the ResizeObserver addon's timer-raced dispatch dam + hidden poll (#16402); swarm-wide carrier audit: #16435.

The Fix (as landed — PR #16436)

Two layers at the owning seam (src/main/addon/DockFlip.mjs):

  1. Hidden entry discriminator: play() returns false (instant landing) at entry when document.hidden === true, before arming any wait — motion is unpresentable there by definition.
  2. #nextFrame timer dam for the occluded class: every former raw rAF await races the native rAF arm against a 100ms timer; both arms are disposed symmetrically (frame win clears the timer; dam win cancels the retained rAF id — review cycle 1 hardening); a starved release tick lands instantly instead of transitioning into a void.

Contract Ledger

Target Surface Source of Authority Behavior Fallback Docs Evidence
DockFlip#play (remote, app-scoped) — hidden documents document.hidden (the RO addon's own arming discriminator) Instant landing at entry: returns false, zero waits armed, no styles touched Committed layout lands raw (presentation-only contract preserved) Class JSDoc two-layer contract Unit instant-lands at entry in a hidden document; WorkstationStarvedTourNL e2e
DockFlip#play — occluded/presenting frame waits (stage A/B, settle, release) Sibling precedent: RO addon timer-raced dam Each wait = rAF-vs-100ms-timer race; BOTH arms disposed (frame win clears timer, dam win cancels rAF id); polls stay count-bounded AND time-bounded Timer arm bounds every wait; worst case = skipped animation under extreme stall #nextFrame JSDoc Unit cancellation witness (registered === cancelled, zero queued); frame-count pins unchanged 15/15
DockFlip#play — starved release tick Presentation-only contract (class doc) !framed → cleanup + false (no transition into a non-painting document; no mid-motion scaled rects held live) Idempotent cleanup restores every temporary style In-line release comment Unit lands instantly when rendering starvation never services the release frame
DockFlip#play — return value Existing JSDoc contract (unchanged) true = animation played to completion; false = any instant-landing path Consumers (below) already treat false as landed play() @returns Existing + new unit suite
DockFlip#captureFirst Unchanged Unchanged (synchronous snapshot) Probe receipt: enters/exits in 1ms under starvation

Consumer families (verified by grep): apps/workstation/view/Workspace.mjs, apps/agentos/view/fleet/FleetCockpit.mjs, apps/agentos/childapps/dockdemo/view/DemoAWorkspace.mjs + DemoBWorkspace.mjs, examples/dashboard/dock/MainContainer.mjs. All call play() inside try/catch and proceed regardless of the boolean — zero consumer changes required.

Acceptance Criteria

  • The starved leg is classified with a receipt (rAF-request stack names DockFlip.mjs stage A; registration triangle exonerated with 31/31 census in rig AND live pane), and the repair lands at that owning seam.
  • Sparkline offscreen registration completes in a rendering-starved document, measured — not delayed-for (witness census assertion; the exonerated mechanism cannot silently regress).
  • The dense tour completes (Tour complete — 11 deterministic beats and 6 surface cues settled.) under the starvation rig.
  • Headed/painting behavior unchanged (all pre-existing unit frame-count pins identical; presenting-mode e2e suite green; headed WorkstationNL failure shown pre-existing on baseline — receipts on #16356).
  • No fixed-delay settlement in the witness (bounded expect.poll only).

Out of Scope

  • Grid geometry acquisition under starvation — #16402 (repaired).
  • The overflow cue's pane-only starved leg — #16434 (the tour's remaining fail-closed stop in genuinely hidden panes).
  • Main-thread timer visibility-throttling carrier audit (incl. RO hidden-poll cadence at the >5min intensive horizon) — #16435.
  • DockFlip same-node-resize choreography — #16391 (closed).

Related

Related: #16402 · #16391 · #16434 · #16435 · #16356 · PR #16436

Original live latest-open sweep preserved: checked latest 20 open issues at 2026-08-03T00:00Z; no equivalent found. Truth-fold applied 2026-08-03 per PR #16436 review cycle 1 (RA-2).

Origin Session ID: 3bdbcbb5-b77b-46f5-88b7-9dbb124733fe

Retrieval Hint: "DockFlip play rendering starvation timer dam hidden discriminator tour wedge"