SPIKE: RFC 957 scheduler interface + adaptive render flushing, on clean main#21518
Draft
NullVoxPopuli-ai-agent wants to merge 6 commits into
Draft
SPIKE: RFC 957 scheduler interface + adaptive render flushing, on clean main#21518NullVoxPopuli-ai-agent wants to merge 6 commits into
NullVoxPopuli-ai-agent wants to merge 6 commits into
Conversation
Adds the `@ember/scheduler` package proposed by RFC 0957: - `render`, `layout`, `composite`, `next` and `idle` phase functions, each returning a promise that resolves according to the registered scheduling strategy - `registerStrategy`, for providing the scheduling strategy when defining the Application - `@ember/scheduler/strategy`, the default strategy implementation, which flushes the render/layout/composite phases in order via ordered requestAnimationFrame callbacks within a single frame, prior to paint The deprecations of @ember/runloop and RSVP described by the RFC are left to follow-up work; this is the additive API surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "composite while composite is flushing" test asserted ordering across
two independent channels: a setTimeout task scheduled during frame 1
versus frame 2's requestAnimationFrame callbacks. The HTML spec does not
order pending timer tasks against the next rendering opportunity, and
Safari 15.6 runs the next frame's rAF callbacks first. The test now
anchors entirely to the rAF channel, using a raw requestAnimationFrame
registered ahead of the rescheduled phase windows as the frame-2
boundary.
idle() also armed requestIdleCallback without a timeout; fully-idle or
backgrounded pages can starve rIC indefinitely, leaving the promise
unresolvable. Cap the wait with { timeout: 500 }.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the race-flush scheduler from the spike stack onto upstream main with no other levers (no subtree-skip, no legacy-read deletion, no iteration fast paths), to isolate what scheduling alone is worth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-dirt-event allocation is gone: the microtask, frame, and revalidate-until-stable callbacks are persistent fields, and the chain leg pays one performance.now() per event instead of two. Dependent chains (render -> effect -> set) re-enter scheduleRevalidate once per step, so closure churn there was measurable GC pressure. The macrotask stand-down now only applies while rAF is being serviced: backgrounded/occluded pages stop firing rAF, and since the per-frame counter is only reset by a frame firing, standing down there stranded all rendering until the tab became visible again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instrumentation showed 146,989 flushes for a 100k-step dependent chain: dirt produced during a flush re-armed a whole new flush even though the flush-until-stable loop had already rendered it, because the scheduled flag cleared before the join. It now clears after, so in-flush dirt dedupes into the running flush. 8x-throttled chain bench: 16.7s -> 5.5s (stock runloop: 3.8s). Chain flushes also inflated the per-frame counter that stands the macrotask leg down, starving misclassified stream dirt of its unclamped leg for up to a full frame (373ms gaps observed under load). Only stream-scheduled flushes count now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A testable build of the "ideal path" for RFC #957: the
@ember/schedulerinterface from #21493 plus the renderer flushing through an adaptive tri-mode scheduler — as the only changes on top of currentmain. No VM changes, no subtree-skipping (#21512), no legacy-read removal (#21513): this isolates what scheduling alone is worth, as evidence for the RFC discussion.Numbers (rere-benchmark, 8x CPU throttle, same-batch vs identically-built
main)Times are anchored on the last DOM mutation (a
MutationObserver), not the benchmark's:donemark —:donerides a barerequestIdleCallbackwhose grant latency adds hundreds of ms of noise for any framework that defers rendering.How the scheduler works
scheduleRevalidateclassifies dirt by arrival latency:queueMicrotask, same task turn, so dependent chains don't pay a task hop per step. This cannot defeat coalescing of awaited update loops: those drain their entire microtask chain before their first flush runs.MessageChannel;setTimeout's 4ms nesting clamp would crawl) andrequestAnimationFrame. Under sustained per-frame bursts the macrotask leg stands down and flushes ride the frame.Hard-won details, each worth a test in the real implementation:
How to test
The
@ember/schedulerphase API (render()/layout()/composite()/next()/idle()+registerStrategy) is included and its tests pass; the renderer's flushing is hardwired inbase-renderer.tsrather than routed through a registered strategy yet — that seam (plus an optional feature flag defaulting to today's backburner behavior,settled()/test-waiter integration, and a sync-flush escape hatch) is the landable follow-up.Expected breakage
This is a spike: the full test suite is not expected green — classic runloop-timing tests assume renders flush synchronously at runloop end. The landable shape puts this behind an optional feature per the RFC's migration story.
The remaining chain-bench gap (5.5s vs 3.8s) is per-step
performance.now()+queueMicrotask+ flush-body overhead vs backburner's tuned autorun; the structural fix is running render-effects inside the flush's own drain loop (the RFC's phase model is exactly that seam — Angular'safterEveryRendershape as a public API).Related: #21493 (interface, mergeable), #21512 (subtree-skip, independent), #21513 (full spike incl. legacy-read removal — that lever turns out unnecessary for the async story).
🤖 Generated with Claude Code