Spike the use-async-scheduler optional feature from RFC 957 - #21554
Spike the use-async-scheduler optional feature from RFC 957#21554NullVoxPopuli-ai-agent wants to merge 1 commit into
use-async-scheduler optional feature from RFC 957#21554Conversation
Adds EmberENV._USE_ASYNC_SCHEDULER (the app-wide optional feature from
RFC 0957's migration roadmap, following the default-async-observers
precedent) as the intermediary migration step: when enabled, Ember's own
scheduling moves onto @ember/scheduler while backburner, RSVP, and the
full @ember/runloop API keep working unchanged for everyone who has not
opted in.
With the flag enabled:
- @ember/runloop is re-implemented on the scheduler: run/join/bind
execute their callback directly, schedule('render'/'afterRender') map
onto the render and layout phases, every other queue becomes a
microtask, next maps onto the next phase, and later/debounce/throttle
become setTimeout-backed timers with the same coalescing semantics.
Backburner is not used at all.
- Rendering is scheduled into the scheduler's render phase (one
deduplicated revalidation pass before the next paint) instead of
backburner's render queue; the reflush loop and renderSettled()
semantics carry over.
- RSVP resolves on the microtask queue like a native promise instead of
joining a runloop.
With the flag disabled (the default) every gated call site falls through
to the exact code that exists today; the full suite is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
rere-benchmark numbers at 4x CPU throttle: ms, lower is better (DB Monitor is fps, higher is better):
Reading:
|
|
Same setup as above, at 8x CPU throttle (medians of 5 interleaved runs, 3 for DB Monitor): ms, lower is better (DB Monitor is fps, higher is better):
8x is out of the headless 60Hz rAF ceiling, so DB Monitor now discriminates: flag on nearly triples fps over main (6.8 → 19.1), with the spike's VM work adding the rest (23.6). Everything else mirrors the 4x picture — flag off ≈ main, flag on takes the async wins (10–20x), small sync benches pay the rAF-flush latency (13.9 → 24.6, 59.3 → 117.2). |
|
Converting to draft since it is a spike |
Spike for the intermediary migration step discussed in the RFC 957 review meeting: one optional feature flag that moves Ember's own scheduling (rendering, runloop callbacks, RSVP flush) onto
@ember/scheduler, while backburner, RSVP, and the full@ember/runloopAPI keep working exactly as today for everyone who has not opted in. Nothing is dropped or deprecated first — apps can absorb the timing change while still running addons that use the runloop, because the runloop API itself is re-implemented on the scheduler.Targets
nvp/scheduler-interface(#21552) since it needs@ember/scheduler.The flag
EmberENV._USE_ASYNC_SCHEDULER, following thedefault-async-observersprecedent (the app-facinguse-async-schedulername would come from@ember/optional-featuresonce this is real). Read lazily at every decision point, so tests can flip it per-module.Flag off (default)
No behavior change: every gated call site falls through to the exact code that exists today. Full suite: 9436 passed, 0 failed.
Flag on
Runloop, re-implemented on the scheduler (
@ember/runloop/-private/scheduler-loop.ts), per the RFC's transition table:run/join/bindonerrorrouting)schedule('render')render()phaseschedule('afterRender')layout()phaseschedule(anything else)Promise.resolve().then(...)scheduleOnce/oncenextnext()phaselater/debounce/throttle/cancelsetTimeout-backed timers with the same coalescing semanticsbegin/endBackburner is not used at all on this path. If the app has not registered a strategy, the default
FrameStrategyis registered on first use.Rendering goes through the scheduler's render phase instead of backburner's
renderqueue:scheduleRevalidate(both the global-context hook and the renderer's own) schedules one deduplicated revalidation pass before the next paint. The reflush loop (_RERENDER_LOOP_LIMIT) andrenderSettled()semantics carry over — the render phase resolves in-window during its own flush, so reflushes stay within the frame.RSVP resolves on the microtask queue like a native promise instead of joining a runloop; unhandled-rejection re-throw happens on a timeout instead of the private error queue. Ember error dispatch (
Ember.onerror/ dispatch override) is unchanged.Tests
packages/@ember/runloop/tests/use_async_scheduler_test.jsruns the runloop surface andrenderSettled()with the flag enabled (14 tests): syncrun/join/bind, microtaskschedule('actions'), phase ordering (actions→render→afterRender),scheduleOnce/oncededupe, cancellation,later/debounce/throttletiming, andrenderSettledresolving with no backburner runloop.Known gaps (why this is a spike)
settled()/test waiters:_hasScheduledTimers()reports the scheduler-backed timers, and the backend tracks all pending work (_hasPendingWork()), but@ember/test-helpersstill watches backburner directly. The RFC's "test waiters observe the scheduler" story is the critical follow-up before the flag is adoptable.actions→routerTransitions→render→afterRender→destroyper loop. Microtask-mapped queues (actions,routerTransitions,destroy, custom queues) now run FIFO in scheduling order rather than queue order.run()no longer forces a synchronous render flush — there is no runloop to flush. Interop relying on "DOM is updated whenrun()returns" needsawait renderSettled().schedulethey request), not at runloop end.