Skip to content

Tidy the bridgeless runtime executor plumbing - #58325

Open
javache wants to merge 2 commits into
react:mainfrom
javache:export-D118616045
Open

Tidy the bridgeless runtime executor plumbing#58325
javache wants to merge 2 commits into
react:mainfrom
javache:export-D118616045

Conversation

@javache

@javache javache commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary:
Follow-ups to the buffered CallInvoker, kept out of that diff so the behavioural
change stays reviewable on its own. No behaviour change intended here.

  • BufferedRuntimeExecutor gains asRuntimeExecutor() and
    asWeakRuntimeExecutor(). RuntimeExecutor is a std::function, so callers
    cannot hand it the object and instead each hand-rolled the same capturing
    lambda; the ownership question now has one answer per lifetime policy. The
    weak form is what getBufferedRuntimeExecutor needs, which must not keep the
    instance alive.
  • BufferedWork becomes private. It was only ever an implementation detail, and
    being public put its fields in the C++ API snapshot.
  • The buffer holds a std::vector sorted once at flush rather than a
    std::priority_queue. Indices are handed out before the lock, so arrival
    order can differ from submission order and something has to restore it — but a
    heap is the expensive way. std::priority_queue::top() returns a const
    reference, so every flushed item copied its std::function on the way out;
    that copy is now a move, and nothing sifts on the way in.
  • runtimeExecutorThatGoesThroughRuntimeScheduler was a second copy of
    getUnbufferedRuntimeExecutor(); it now calls it.
  • RuntimeSchedulerCallInvoker is marked deprecated. Its remaining users are
    migrated: ReactCxxPlatform holds a ReactInstance and can use
    createJSCallInvoker(), and four other files only #included it without ever
    constructing one. The single remaining use is the flag-off branch, which is
    suppressed locally and goes when the flag is cleaned up.
  • Drops a null check on bufferedRuntimeExecutor_ in callFunctionOnModule
    that the constructor makes unreachable.

Changelog:
[Internal]

Differential Revision: D118616045

Summary:
In bridgeless, native reaches JS by two routes that end in the same
`RuntimeScheduler` queue but get there differently. `callFunctionOnModule` goes
through the instance's `BufferedRuntimeExecutor`; the `CallInvoker` goes
straight to `scheduleTask`. The CallInvoker therefore skips the buffer entirely
and can reach the runtime while a module call issued earlier is still parked,
unflushed, because the bundle is mid-evaluation. Native code that issues both
cannot rely on the order it issued them in, and `Task` is a min-heap on
`now() + timeout(priority)` with no insertion tiebreak, so equal priorities do
not settle it either.

Gives the two channels the same buffering. `BufferedRuntimeExecutor` gains a
priority-carrying `execute`, so work routed through it keeps the scheduler
priority it was submitted with instead of collapsing to the executor default,
and buffered work from both overloads stays in one submission-ordered stream.
`BufferedCallInvoker` sits on that executor and becomes the bridgeless
`jsCallInvoker` on Android, iOS and macOS.

`invokeSync` deliberately keeps going straight to the scheduler: a synchronous
call cannot wait for a flush that only happens once the bundle has run.

Behind `enableBufferedCallInvoker`, default true. `ReactInstance` picks between
the buffered invoker and the existing `RuntimeSchedulerCallInvoker` in one
place, so the platform call sites are identical either way and the change is
revertible at runtime — it moves when native-issued async work first reaches JS
during startup, which is the intended contract but affects every native module.

One lifetime hazard this surfaces, worth knowing about beyond this diff:
`BufferedRuntimeExecutor` reaches the scheduler through a raw pointer captured
at construction, which is safe only while the owning instance is alive. A
CallInvoker is routinely held across instance teardown, so `BufferedCallInvoker`
guards every async dispatch on a weak reference to the scheduler and drops the
work when it has expired — the same contract `RuntimeSchedulerCallInvoker` has.
Without that guard this reliably segfaults on a reload.

Changelog:
[General][Changed] - Async `CallInvoker` work is now buffered alongside callable module calls, so it no longer runs before the JS bundle has finished evaluating

Differential Revision: D118456662
Summary:
Follow-ups to the buffered CallInvoker, kept out of that diff so the behavioural
change stays reviewable on its own. No behaviour change intended here.

- `BufferedRuntimeExecutor` gains `asRuntimeExecutor()` and
  `asWeakRuntimeExecutor()`. RuntimeExecutor is a std::function, so callers
  cannot hand it the object and instead each hand-rolled the same capturing
  lambda; the ownership question now has one answer per lifetime policy. The
  weak form is what `getBufferedRuntimeExecutor` needs, which must not keep the
  instance alive.
- `BufferedWork` becomes private. It was only ever an implementation detail, and
  being public put its fields in the C++ API snapshot.
- The buffer holds a `std::vector` sorted once at flush rather than a
  `std::priority_queue`. Indices are handed out before the lock, so arrival
  order can differ from submission order and something has to restore it — but a
  heap is the expensive way. `std::priority_queue::top()` returns a const
  reference, so every flushed item copied its `std::function` on the way out;
  that copy is now a move, and nothing sifts on the way in.
- `runtimeExecutorThatGoesThroughRuntimeScheduler` was a second copy of
  `getUnbufferedRuntimeExecutor()`; it now calls it.
- `RuntimeSchedulerCallInvoker` is marked deprecated. Its remaining users are
  migrated: `ReactCxxPlatform` holds a `ReactInstance` and can use
  `createJSCallInvoker()`, and four other files only `#include`d it without ever
  constructing one. The single remaining use is the flag-off branch, which is
  suppressed locally and goes when the flag is cleaned up.
- Drops a null check on `bufferedRuntimeExecutor_` in `callFunctionOnModule`
  that the constructor makes unreachable.

Changelog:
[Internal]

Differential Revision: D118616045
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 4, 2026
@facebook-github-tools facebook-github-tools Bot added p: Facebook Partner: Facebook Partner labels Sep 4, 2026
@meta-codesync

meta-codesync Bot commented Sep 4, 2026

Copy link
Copy Markdown

@javache has exported this pull request. If you are a Meta employee, you can view the originating Diff in D118616045.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant