Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

fix(RuntimeScheduler): flush JS microtasks after work-loop to prevent…#85

Merged
SudoPlz merged 1 commit into
discord:0.78.0-discordfrom
SudoPlz:sudoplz/fix-runtime-scheduler-flush-microtasks
Jul 23, 2025
Merged

fix(RuntimeScheduler): flush JS microtasks after work-loop to prevent…#85
SudoPlz merged 1 commit into
discord:0.78.0-discordfrom
SudoPlz:sudoplz/fix-runtime-scheduler-flush-microtasks

Conversation

@SudoPlz

@SudoPlz SudoPlz commented Jul 22, 2025

Copy link
Copy Markdown

🐛 Fabric + Reanimated microtasks never flush (animations freeze)

TL;DR
React-Freeze’s infiniteThenable leaves Fabric’s RuntimeScheduler_Legacy stuck in an endless work-loop.
JS microtasks never flush → Reanimated worklets never run → all animations stall.
This issue proposes calling performMicrotaskCheckpoint(runtime) inside the loop so the JS queue is processed each iteration.


Symptoms

⚠️ What we saw Notes
Animations randomly freeze Reanimated worklets never execute.
global.queueMicrotask logs enqueue events but global.__callMicrotasks never logs execution.
Adding a lone console.log “fixes” the bug crossing the bridge forces a microtask flush (Heisenbug).

Investigation summary

  1. React-Freeze (v1.0.4) suspends trees with an infiniteThenable.
  2. React repeatedly calls commitRootImpl → schedules tasks via Scheduler.unstable_scheduleCallback.
  3. Native side enters RuntimeScheduler_Legacy::startWorkLoop and spins forever:
while (!taskQueue_.empty()) {
  // process task …
}
  1. Because the loop never finishes, JS is never asked to flush its microtasks (MessageQueue.__callReactNativeMicrotasks).
  2. Console logs appear to “fix” the bug because they cross the RN bridge and force a JS flush.

Proposed fix (implemented in PR)

performMicrotaskCheckpoint(runtime);          // NEW
  • performMicrotaskCheckpoint calls the global _flushReactNativeMicrotasks JS helper (already registered by JSTimers) which empties the queue via JSTimers.callReactNativeMicrotasks().

Why this is safe

  • Only runs after a task is executed (identical spot where the queue would normally be flushed if the loop exited).
  • No-op cost when queue is empty.
  • Works in classic bridge and Fabric-bridgeless.
  • Is actually the way the standard modern runtime scheduler works (when in bridgeless)

Ticket

https://app.asana.com/1/236888843494340/project/1199705967702853/task/1210188774106277?focus=true


Credits

Big shout-out to Szymon for spotting the infinite loop and mapping the bridge call-chain! 🎉

… Reanimated stalls

React-Freeze’s infiniteThenable causes Fabric’s RuntimeScheduler_Legacy
to spin indefinitely within the while loop within startWorkLoop, which prevents the JS
thread from ever flushing its microtask queue (handled by
JSTimers/MessageQueue).  When the queue starves, Reanimated worklets never
run and animations freeze.

This patch adds an explicit call to performMicrotaskCheckpoint(runtime)
immediately after each task execution inside startWorkLoop.
The helper invokes the global _flushReactNativeMicrotasks JS function
(registered from JSTimers) so the queued microtasks are processed even while
the native work-loop is busy.

✓  Restores Reanimated animations under Fabric bridgeless

ticket: https://app.asana.com/1/236888843494340/project/1199705967702853/task/1210188774106277?focus=true

@Flewp Flewp left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 🎉

Is this upstream-able? Should it be?
Any expected impact on CPU usage?

@SudoPlz

SudoPlz commented Jul 22, 2025

Copy link
Copy Markdown
Author

@Flewp yes it's upstreamable, I didn't notice any impact on CPU usage, essentially it's flushing the microtask queue after every task completion, which is the same thing the modern RuntimeScheduler does as well.

This PR is bringing that change to the Legacy runtime scheduler (the one we're using now).

After we turn bridgeless mode on, we will be using the modern RuntimeScheduler and this change will be useless, but for the time being we need to flush the micro task queue manually because of react-freeze.

@SudoPlz SudoPlz merged commit 64fcffb into discord:0.78.0-discord Jul 23, 2025
29 of 51 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants