From ebee67024534ac270070e74c3c9f8b6d366918e9 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 24 Aug 2026 17:42:22 -0400 Subject: [PATCH 1/3] Process synchronous event beats in the frame that requested them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EventEmitter::experimental_flushSync` only *requests* a beat, which is processed at the next `EventBeat::induce`. On Android the induce happens within the frame, before drawing, so a synchronous request made during layout is processed in that frame. On iOS it is not: the run loop observer that induces the beat runs before Core Animation's commit observer, so a request made from `layoutSubviews` — inside CA's commit cycle — is only processed one frame later. `AppleEventBeat` now also schedules an induce in the display phase of the current commit cycle. Core Animation runs a commit as layout → display → commit, so a zero-sized layer marked as needing display during layout has its `display` called after the whole layout pass and before the transaction is committed. A layer is kept in every visible window of every foreground scene, since the request can come from any of them — a modal and the LogBox are windows of their own — and only a layer in the tree being committed is guaranteed a display this cycle. Requests within one cycle coalesce into a single induce, so mounting ten observing views is one beat rather than ten. Two related fixes in `EventBeat` itself: a synchronous request is no longer stranded behind an already-scheduled asynchronous beat (it would silently lose its this-frame guarantee, and the leftover flag would make an unrelated later beat blocking), and `induce` becomes public so platform beats can call it from a callback. `AppleEventBeat.cpp` becomes `.mm` for the Objective-C. Covered by new unit tests in `EventBeatTest.cpp`. This is the platform half of the safe area insets work: it is what makes an inset change reported from `layoutSubviews` render in the frame it happened in. `VirtualView` uses the same mechanism. --- .../React/Fabric/AppleEventBeat.cpp | 31 --- .../React/Fabric/AppleEventBeat.h | 14 ++ .../React/Fabric/AppleEventBeat.mm | 182 ++++++++++++++++++ .../react/renderer/core/EventBeat.cpp | 9 +- .../react/renderer/core/EventBeat.h | 16 +- .../runtimescheduler/tests/EventBeatTest.cpp | 168 ++++++++++++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 2 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 2 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 4 +- .../api-snapshots/ReactAppleNewarchCxx.api | 4 +- .../api-snapshots/ReactAppleReleaseCxx.api | 4 +- .../api-snapshots/ReactCommonDebugCxx.api | 2 +- .../api-snapshots/ReactCommonNewarchCxx.api | 2 +- .../api-snapshots/ReactCommonReleaseCxx.api | 2 +- 15 files changed, 397 insertions(+), 47 deletions(-) delete mode 100644 packages/react-native/React/Fabric/AppleEventBeat.cpp create mode 100644 packages/react-native/React/Fabric/AppleEventBeat.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp diff --git a/packages/react-native/React/Fabric/AppleEventBeat.cpp b/packages/react-native/React/Fabric/AppleEventBeat.cpp deleted file mode 100644 index 4a3d533a0cd9..000000000000 --- a/packages/react-native/React/Fabric/AppleEventBeat.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include "AppleEventBeat.h" - -#include - -namespace facebook::react { - -AppleEventBeat::AppleEventBeat( - std::shared_ptr ownerBox, - std::unique_ptr uiRunLoopObserver, - RuntimeScheduler& runtimeScheduler) - : EventBeat(std::move(ownerBox), runtimeScheduler), - uiRunLoopObserver_(std::move(uiRunLoopObserver)) { - uiRunLoopObserver_->setDelegate(this); - uiRunLoopObserver_->enable(); -} - -void AppleEventBeat::activityDidChange( - const RunLoopObserver::Delegate* delegate, - RunLoopObserver::Activity /*activity*/) const noexcept { - react_native_assert(delegate == this); - induce(); -} - -} // namespace facebook::react diff --git a/packages/react-native/React/Fabric/AppleEventBeat.h b/packages/react-native/React/Fabric/AppleEventBeat.h index 256e0f0983ad..528145e23797 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.h +++ b/packages/react-native/React/Fabric/AppleEventBeat.h @@ -7,6 +7,8 @@ #pragma once +#include + #include #include #include @@ -19,6 +21,11 @@ class RuntimeScheduler; * Event beat associated with JavaScript runtime. * The beat is called on `RuntimeExecutor`'s thread induced by the UI thread * event loop. + * + * A synchronous request made while Core Animation is laying out the current + * frame (the run loop observer that induces the beat has already run at that + * point) is additionally induced from the display phase of the same commit + * cycle, so that its effects are mounted before the frame is presented. */ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { public: @@ -27,13 +34,20 @@ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { std::unique_ptr uiRunLoopObserver, RuntimeScheduler &RuntimeScheduler); + ~AppleEventBeat() override; + + void requestSynchronous() const override; + #pragma mark - RunLoopObserver::Delegate void activityDidChange(const RunLoopObserver::Delegate *delegate, RunLoopObserver::Activity activity) const noexcept override; private: + class DisplayPhaseFlusher; + std::unique_ptr uiRunLoopObserver_; + std::unique_ptr displayPhaseFlusher_; }; } // namespace facebook::react diff --git a/packages/react-native/React/Fabric/AppleEventBeat.mm b/packages/react-native/React/Fabric/AppleEventBeat.mm new file mode 100644 index 000000000000..b91adb43c2d6 --- /dev/null +++ b/packages/react-native/React/Fabric/AppleEventBeat.mm @@ -0,0 +1,182 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AppleEventBeat.h" + +#import +#import + +#include + +/* + * A zero-sized layer whose only purpose is to run a callback during the + * display phase of a Core Animation commit. Core Animation processes a commit + * as layout → display → (repeat until stable) → commit, so a layer marked as + * needing display during the layout phase has its `display` called after the + * whole layout pass but before the transaction is committed. + */ +@interface RCTEventBeatFlusherLayer : CALayer +@property (nonatomic, copy, nullable) void (^onDisplay)(void); +@end + +@implementation RCTEventBeatFlusherLayer + +- (void)display +{ + if (self.onDisplay != nil) { + self.onDisplay(); + } +} + +// The layer is not a visual element; never participate in animations. +- (id)actionForKey:(NSString *)event +{ + return nil; +} + +@end + +/* + * The windows that can commit a Core Animation transaction: the visible ones + * of every foreground scene. + */ +static NSArray *RCTFlushableWindows(void) +{ + NSMutableArray *windows = [NSMutableArray new]; + for (UIScene *scene in RCTSharedApplication().connectedScenes) { + if (![scene isKindOfClass:[UIWindowScene class]]) { + continue; + } + if (scene.activationState != UISceneActivationStateForegroundActive && + scene.activationState != UISceneActivationStateForegroundInactive) { + continue; + } + for (UIWindow *window in ((UIWindowScene *)scene).windows) { + if (!window.hidden) { + [windows addObject:window]; + } + } + } + if (windows.count == 0) { + // Apps on the legacy UIApplicationDelegate lifecycle own their window + // outside of any scene, so the enumeration above finds nothing. + UIWindow *keyWindow = RCTKeyWindow(); + if (keyWindow != nil) { + [windows addObject:keyWindow]; + } + } + return windows; +} + +namespace facebook::react { + +/* + * Owns the flusher layers and keeps one attached to every window's layer so + * that whichever layer tree is being committed contains one of them. + */ +class AppleEventBeat::DisplayPhaseFlusher { + public: + DisplayPhaseFlusher(std::function callback, std::weak_ptr weakOwner) + { + // Weak keys: a window that goes away takes its own layer with it. + layers_ = [NSMapTable weakToStrongObjectsMapTable]; + auto sharedCallback = std::make_shared>(std::move(callback)); + onDisplay_ = ^{ + // The owner (indirectly) retains the event beat; if it is gone, so is + // the beat the callback points into. + auto owner = weakOwner.lock(); + if (!owner) { + return; + } + (*sharedCallback)(); + }; + } + + ~DisplayPhaseFlusher() + { + // The beat can be destroyed on any thread; layer mutations belong on the + // main thread. The block only retains the layers, and a display happening + // before this executes is made safe by the owner check above. + NSMapTable *layers = layers_; + RCTExecuteOnMainQueue(^{ + for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) { + layer.onDisplay = nil; + [layer removeFromSuperlayer]; + } + [layers removeAllObjects]; + }); + } + + /* + * Schedules the callback to run in the display phase of the current (or + * next) Core Animation commit cycle. Main thread only. + * + * Every window gets a layer rather than only the key window: the request can + * come from any of them — a modal and the LogBox are windows of their own — + * and only a layer in a tree that is committed is displayed in this cycle. + * The induce the display triggers is coalescing, so the extra layers cost a + * dirty zero-sized layer each, not extra beats. + */ + void schedule() const + { + for (UIWindow *window in RCTFlushableWindows()) { + RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:window]; + if (layer == nil) { + layer = [RCTEventBeatFlusherLayer new]; + layer.frame = CGRectZero; + layer.onDisplay = onDisplay_; + [layers_ setObject:layer forKey:window]; + } + if (layer.superlayer != window.layer) { + [window.layer addSublayer:layer]; + } + [layer setNeedsDisplay]; + } + } + + private: + NSMapTable *layers_; + void (^onDisplay_)(void); +}; + +AppleEventBeat::AppleEventBeat(std::shared_ptr ownerBox, + std::unique_ptr uiRunLoopObserver, + RuntimeScheduler &runtimeScheduler) + : EventBeat(std::move(ownerBox), runtimeScheduler), + uiRunLoopObserver_(std::move(uiRunLoopObserver)), + displayPhaseFlusher_(std::make_unique([this]() { induce(); }, ownerBox_->owner)) +{ + uiRunLoopObserver_->setDelegate(this); + uiRunLoopObserver_->enable(); +} + +AppleEventBeat::~AppleEventBeat() = default; + +void AppleEventBeat::requestSynchronous() const +{ + EventBeat::requestSynchronous(); + + // The run loop observer that ordinarily induces the beat runs before Core + // Animation commits the frame. A synchronous request made while Core + // Animation is already laying out (e.g. an event emitted from + // `layoutSubviews`) would therefore only be processed on the next frame. + // Scheduling an induce in the display phase of the current commit cycle + // processes it before this frame is presented. Multiple requests within one + // cycle coalesce into a single induce. + if (RCTIsMainQueue()) { + displayPhaseFlusher_->schedule(); + } +} + +void AppleEventBeat::activityDidChange(const RunLoopObserver::Delegate *delegate, + RunLoopObserver::Activity /*activity*/) const noexcept +{ + react_native_assert(delegate == this); + induce(); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp index cdb05f4719fd..839e2f19f90b 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp @@ -53,7 +53,14 @@ void EventBeat::induce() const { isEventBeatRequested_ = false; if (isBeatCallbackScheduled_) { - return; + // An asynchronous beat is already scheduled but has not run yet. A + // synchronous request must not be stranded behind it (it would silently + // lose its this-frame guarantee, and the leftover flag would make an + // unrelated later beat blocking), so it proceeds and processes the queue + // now; the already scheduled beat will simply find an empty queue. + if (!isSynchronousRequested_) { + return; + } } isBeatCallbackScheduled_ = true; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h index 3740b457e785..b8da33cc9ac2 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h @@ -111,6 +111,16 @@ class EventBeat { */ virtual void requestSynchronous() const; + /* + * Induces the next beat to happen as soon as possible. + * Receiver might ignore the call if a beat was not requested. + * + * Ordinarily called by the platform once per frame; also callable by a + * consumer right after `requestSynchronous` to process the queue immediately + * at the call site instead of at the next frame boundary. + */ + void induce() const; + /* * The callback will be executed once a consumer (for example EventQueue) * calls either `EventBeat::request` or `EventBeat::requestSynchronous`. The @@ -128,12 +138,6 @@ class EventBeat { void unstable_setInduceCallback(std::function callback); protected: - /* - * Induces the next beat to happen as soon as possible. - * Receiver might ignore the call if a beat was not requested. - */ - void induce() const; - BeatCallback beatCallback_; std::function induceCallback_; std::shared_ptr ownerBox_; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp new file mode 100644 index 000000000000..1dda2fd47fd6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "StubQueue.h" + +namespace facebook::react { + +class EventBeatTestFeatureFlags : public ReactNativeFeatureFlagsDefaults { + public: + bool enableBridgelessArchitecture() override { + return true; + } +}; + +class EventBeatTest : public testing::Test { + protected: + void SetUp() override { + ReactNativeFeatureFlags::dangerouslyReset(); + ReactNativeFeatureFlags::override( + std::make_unique()); + + runtime_ = facebook::hermes::makeHermesRuntime( + ::hermes::vm::RuntimeConfig::Builder().build()); + stubQueue_ = std::make_unique(); + + RuntimeExecutor runtimeExecutor = + [this]( + std::function&& callback) { + stubQueue_->runOnQueue([this, callback = std::move(callback)]() { + callback(*runtime_); + }); + }; + + runtimeScheduler_ = std::make_unique(runtimeExecutor); + + ownerBox_ = std::make_shared(); + owner_ = std::make_shared(0); + ownerBox_->owner = owner_; + eventBeat_ = std::make_unique(ownerBox_, *runtimeScheduler_); + } + + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } + + std::unique_ptr runtime_; + std::unique_ptr stubQueue_; + std::unique_ptr runtimeScheduler_; + std::shared_ptr ownerBox_; + std::shared_ptr owner_; + std::unique_ptr eventBeat_; +}; + +TEST_F(EventBeatTest, induceWithoutRequestIsNoop) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + eventBeat_->induce(); + + EXPECT_EQ(beatCount, 0); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_F(EventBeatTest, synchronousRequestIsProcessedAtInduce) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + eventBeat_->requestSynchronous(); + EXPECT_EQ(beatCount, 0); + + // Platform implementations induce the beat at a point where the effects of + // synchronous events can still make the current frame (the display phase on + // Apple, before the draw on Android). The beat callback runs synchronously + // before `induce` returns, with both threads blocked. + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 1); + + // The request was consumed: another induce does nothing. + eventBeat_->induce(); + EXPECT_EQ(beatCount, 1); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_F(EventBeatTest, requestMadeDuringBeatIsProcessedByASubsequentInduce) { + int beatCount = 0; + eventBeat_->setBeatCallback([&](jsi::Runtime& /*runtime*/) { + beatCount++; + if (beatCount == 1) { + // A synchronous request made from within the beat (e.g. an event whose + // handler causes another synchronous event). Platform implementations + // defer the induce for it (the display phase flusher on Apple, the next + // pre-draw on Android) rather than inducing from within the beat. + eventBeat_->requestSynchronous(); + } + }); + + eventBeat_->requestSynchronous(); + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 1); + + // The request made during the beat is not lost: the next induce processes + // it. + std::thread driver2([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver2.join(); + + EXPECT_EQ(beatCount, 2); +} + +TEST_F(EventBeatTest, synchronousRequestIsNotStrandedBehindScheduledBeat) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + // An asynchronous beat is scheduled but has not run yet. + eventBeat_->request(); + eventBeat_->induce(); + EXPECT_EQ(beatCount, 0); + + // A synchronous request arriving now must still be processed by its induce + // instead of being silently deferred behind the scheduled beat. + eventBeat_->requestSynchronous(); + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 2); +} + +} // namespace facebook::react diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2e6dc7e0b4e2..2531a123e50e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2218,7 +2218,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -2227,6 +2226,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 7ec351405ee3..3d346ac488cf 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2201,7 +2201,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -2210,6 +2209,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6843410835c3..130a86d9588b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2216,7 +2216,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -2225,6 +2224,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d6dc3f80a6ad..90f9105cb7e4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -4245,6 +4245,8 @@ class facebook::react::AppRegistryBinding { class facebook::react::AppleEventBeat : public facebook::react::EventBeat, public facebook::react::RunLoopObserver::Delegate { public AppleEventBeat(std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, facebook::react::RuntimeScheduler& RuntimeScheduler); public virtual void activityDidChange(const facebook::react::RunLoopObserver::Delegate* delegate, facebook::react::RunLoopObserver::Activity activity) const noexcept override; + public virtual void requestSynchronous() const override; + public ~AppleEventBeat() override; } class facebook::react::AsyncArrayBuffer { @@ -4745,7 +4747,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -4754,6 +4755,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index da7a542fd69f..49dcfe05d451 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -4232,6 +4232,8 @@ class facebook::react::AppRegistryBinding { class facebook::react::AppleEventBeat : public facebook::react::EventBeat, public facebook::react::RunLoopObserver::Delegate { public AppleEventBeat(std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, facebook::react::RuntimeScheduler& RuntimeScheduler); public virtual void activityDidChange(const facebook::react::RunLoopObserver::Delegate* delegate, facebook::react::RunLoopObserver::Activity activity) const noexcept override; + public virtual void requestSynchronous() const override; + public ~AppleEventBeat() override; } class facebook::react::AsyncArrayBuffer { @@ -4721,7 +4723,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -4730,6 +4731,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index a92742d8c14e..0059a9259d0f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -4243,6 +4243,8 @@ class facebook::react::AppRegistryBinding { class facebook::react::AppleEventBeat : public facebook::react::EventBeat, public facebook::react::RunLoopObserver::Delegate { public AppleEventBeat(std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, facebook::react::RuntimeScheduler& RuntimeScheduler); public virtual void activityDidChange(const facebook::react::RunLoopObserver::Delegate* delegate, facebook::react::RunLoopObserver::Activity activity) const noexcept override; + public virtual void requestSynchronous() const override; + public ~AppleEventBeat() override; } class facebook::react::AsyncArrayBuffer { @@ -4743,7 +4745,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -4752,6 +4753,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 763c902b2e27..9d716ed5a43e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -1460,7 +1460,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -1469,6 +1468,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index abad9815f5c4..dcd3dcec5dde 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -1444,7 +1444,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -1453,6 +1452,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 0c58351b5c06..65b495bc9243 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -1458,7 +1458,6 @@ class facebook::react::EventBeat { protected mutable std::atomic isEventBeatRequested_; protected std::function induceCallback_; protected std::shared_ptr ownerBox_; - protected void induce() const; public EventBeat(const facebook::react::EventBeat& other) = delete; public EventBeat(std::shared_ptr ownerBox, facebook::react::RuntimeScheduler& runtimeScheduler); public facebook::react::EventBeat& operator=(const facebook::react::EventBeat& other) = delete; @@ -1467,6 +1466,7 @@ class facebook::react::EventBeat { public virtual void request() const; public virtual void requestSynchronous() const; public virtual ~EventBeat() = default; + public void induce() const; public void setBeatCallback(facebook::react::EventBeat::BeatCallback beatCallback); } From dd5c6d7ed9959e077b0c3992926470aa1821360a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 14 Sep 2026 15:48:22 -0400 Subject: [PATCH 2/3] Make the stranded-synchronous-request test deterministic The driver ticked the queue exactly twice, but the number of queued tasks depends on thread interleaving: when the synchronous access request is queued before the first tick, the scheduled beat's work item yields without executing and is re-queued for a third tick that never came. Drive the queue from the test thread with the same waitForTasks synchronization the RuntimeScheduler tests use, asserting each stage. --- .../runtimescheduler/tests/EventBeatTest.cpp | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp index 1dda2fd47fd6..dbef2bcd6c37 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp @@ -153,16 +153,26 @@ TEST_F(EventBeatTest, synchronousRequestIsNotStrandedBehindScheduledBeat) { // A synchronous request arriving now must still be processed by its induce // instead of being silently deferred behind the scheduled beat. eventBeat_->requestSynchronous(); - std::thread driver([this]() { - stubQueue_->waitForTask(); - stubQueue_->tick(); - stubQueue_->waitForTask(); - stubQueue_->tick(); - }); - eventBeat_->induce(); - driver.join(); + std::thread inducer([this]() { eventBeat_->induce(); }); + + // Wait until the synchronous access request joins the already-queued work + // item, so that the tick order below is deterministic. + stubQueue_->waitForTasks(2); + + // The scheduled beat's work item yields to the pending synchronous access + // without executing. + stubQueue_->tick(); + EXPECT_EQ(beatCount, 0); + // The synchronous access processes the beat within its induce. + stubQueue_->tick(); + inducer.join(); + EXPECT_EQ(beatCount, 1); + + // The beat that yielded resumes afterwards; it was not lost. + stubQueue_->tick(); EXPECT_EQ(beatCount, 2); + EXPECT_EQ(stubQueue_->size(), 0); } } // namespace facebook::react From ad3233ec5968130acceaa424b568082b3afc7f4e Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 14 Sep 2026 18:35:52 -0400 Subject: [PATCH 3/3] Prototype: schedule the display-phase induce on the requesting surface experimental_flushSync now carries the surface of the emitter that requested it (from the ShadowNodeFamily every emitter is given at creation) through EventDispatcher and EventQueue to EventBeat::requestSynchronous, as an optional: without a surface the induce falls back to the run loop observer's ordinary schedule. AppleEventBeat resolves the surface to its root view's layer via a resolver injected by RCTSurfacePresenter from the surface registry, and attaches the flusher layer there instead of to every visible window. A request made from layout runs inside the commit of exactly that tree, so the layer is guaranteed a display phase in the same cycle without assuming all windows commit in one transaction. VirtualView's sync flushes get the same targeting through its own emitter, unchanged. --- .../React/Fabric/AppleEventBeat.h | 19 ++- .../React/Fabric/AppleEventBeat.mm | 109 ++++++++---------- .../React/Fabric/RCTSurfacePresenter.mm | 13 ++- .../react/renderer/core/EventBeat.cpp | 3 +- .../react/renderer/core/EventBeat.h | 10 +- .../react/renderer/core/EventDispatcher.cpp | 5 +- .../react/renderer/core/EventDispatcher.h | 4 +- .../react/renderer/core/EventEmitter.cpp | 10 ++ .../react/renderer/core/EventEmitter.h | 10 +- .../react/renderer/core/EventQueue.cpp | 5 +- .../react/renderer/core/EventQueue.h | 4 +- 11 files changed, 113 insertions(+), 79 deletions(-) diff --git a/packages/react-native/React/Fabric/AppleEventBeat.h b/packages/react-native/React/Fabric/AppleEventBeat.h index 528145e23797..3b0e42aa1af0 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.h +++ b/packages/react-native/React/Fabric/AppleEventBeat.h @@ -7,7 +7,11 @@ #pragma once +#include #include +#include + +#import #include #include @@ -25,18 +29,27 @@ class RuntimeScheduler; * A synchronous request made while Core Animation is laying out the current * frame (the run loop observer that induces the beat has already run at that * point) is additionally induced from the display phase of the same commit - * cycle, so that its effects are mounted before the frame is presented. + * cycle, so that its effects are mounted before the frame is presented. The + * induce is scheduled on the layer of the requesting surface's root view — + * the tree Core Animation is laying out when the request is made from layout. */ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { public: + /* + * Resolves the layer of a surface's root view. Called on the main thread; + * returns nil when the surface is unknown or its view is not mounted. + */ + using SurfaceLayerResolver = std::function; + AppleEventBeat( std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, - RuntimeScheduler &RuntimeScheduler); + RuntimeScheduler &RuntimeScheduler, + SurfaceLayerResolver surfaceLayerResolver); ~AppleEventBeat() override; - void requestSynchronous() const override; + void requestSynchronous(std::optional surfaceId) const override; #pragma mark - RunLoopObserver::Delegate diff --git a/packages/react-native/React/Fabric/AppleEventBeat.mm b/packages/react-native/React/Fabric/AppleEventBeat.mm index b91adb43c2d6..00d2956e2f42 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.mm +++ b/packages/react-native/React/Fabric/AppleEventBeat.mm @@ -40,49 +40,25 @@ - (void)display @end -/* - * The windows that can commit a Core Animation transaction: the visible ones - * of every foreground scene. - */ -static NSArray *RCTFlushableWindows(void) -{ - NSMutableArray *windows = [NSMutableArray new]; - for (UIScene *scene in RCTSharedApplication().connectedScenes) { - if (![scene isKindOfClass:[UIWindowScene class]]) { - continue; - } - if (scene.activationState != UISceneActivationStateForegroundActive && - scene.activationState != UISceneActivationStateForegroundInactive) { - continue; - } - for (UIWindow *window in ((UIWindowScene *)scene).windows) { - if (!window.hidden) { - [windows addObject:window]; - } - } - } - if (windows.count == 0) { - // Apps on the legacy UIApplicationDelegate lifecycle own their window - // outside of any scene, so the enumeration above finds nothing. - UIWindow *keyWindow = RCTKeyWindow(); - if (keyWindow != nil) { - [windows addObject:keyWindow]; - } - } - return windows; -} - namespace facebook::react { /* - * Owns the flusher layers and keeps one attached to every window's layer so - * that whichever layer tree is being committed contains one of them. + * Owns the flusher layers, one per surface that has made a synchronous + * request, attached to the layer of that surface's root view. A request made + * from layout runs inside the commit of exactly that tree, so its layer is + * guaranteed a display phase in the current cycle — no assumption about + * which window is key or about all windows committing together. */ class AppleEventBeat::DisplayPhaseFlusher { public: - DisplayPhaseFlusher(std::function callback, std::weak_ptr weakOwner) + DisplayPhaseFlusher( + std::function callback, + std::weak_ptr weakOwner, + SurfaceLayerResolver surfaceLayerResolver) + : surfaceLayerResolver_(std::move(surfaceLayerResolver)) { - // Weak keys: a window that goes away takes its own layer with it. + // Weak keys: a root view that goes away takes its own flusher layer with + // it. layers_ = [NSMapTable weakToStrongObjectsMapTable]; auto sharedCallback = std::make_shared>(std::move(callback)); onDisplay_ = ^{ @@ -101,7 +77,7 @@ - (void)display // The beat can be destroyed on any thread; layer mutations belong on the // main thread. The block only retains the layers, and a display happening // before this executes is made safe by the owner check above. - NSMapTable *layers = layers_; + NSMapTable *layers = layers_; RCTExecuteOnMainQueue(^{ for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) { layer.onDisplay = nil; @@ -113,42 +89,47 @@ - (void)display /* * Schedules the callback to run in the display phase of the current (or - * next) Core Animation commit cycle. Main thread only. - * - * Every window gets a layer rather than only the key window: the request can - * come from any of them — a modal and the LogBox are windows of their own — - * and only a layer in a tree that is committed is displayed in this cycle. - * The induce the display triggers is coalescing, so the extra layers cost a - * dirty zero-sized layer each, not extra beats. + * next) Core Animation commit cycle, on the layer tree of the surface's + * root view. Main thread only. Does nothing when the surface has no mounted + * view; the run loop observer then processes the request on its ordinary + * schedule instead. */ - void schedule() const + void schedule(SurfaceId surfaceId) const { - for (UIWindow *window in RCTFlushableWindows()) { - RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:window]; - if (layer == nil) { - layer = [RCTEventBeatFlusherLayer new]; - layer.frame = CGRectZero; - layer.onDisplay = onDisplay_; - [layers_ setObject:layer forKey:window]; - } - if (layer.superlayer != window.layer) { - [window.layer addSublayer:layer]; - } - [layer setNeedsDisplay]; + CALayer *hostLayer = surfaceLayerResolver_ ? surfaceLayerResolver_(surfaceId) : nil; + if (hostLayer == nil) { + return; + } + RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:hostLayer]; + if (layer == nil) { + layer = [RCTEventBeatFlusherLayer new]; + layer.frame = CGRectZero; + layer.onDisplay = onDisplay_; + [layers_ setObject:layer forKey:hostLayer]; + } + if (layer.superlayer != hostLayer) { + [layer removeFromSuperlayer]; + [hostLayer addSublayer:layer]; } + [layer setNeedsDisplay]; } private: - NSMapTable *layers_; + SurfaceLayerResolver surfaceLayerResolver_; + NSMapTable *layers_; void (^onDisplay_)(void); }; AppleEventBeat::AppleEventBeat(std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, - RuntimeScheduler &runtimeScheduler) + RuntimeScheduler &runtimeScheduler, + SurfaceLayerResolver surfaceLayerResolver) : EventBeat(std::move(ownerBox), runtimeScheduler), uiRunLoopObserver_(std::move(uiRunLoopObserver)), - displayPhaseFlusher_(std::make_unique([this]() { induce(); }, ownerBox_->owner)) + displayPhaseFlusher_(std::make_unique( + [this]() { induce(); }, + ownerBox_->owner, + std::move(surfaceLayerResolver))) { uiRunLoopObserver_->setDelegate(this); uiRunLoopObserver_->enable(); @@ -156,9 +137,9 @@ void schedule() const AppleEventBeat::~AppleEventBeat() = default; -void AppleEventBeat::requestSynchronous() const +void AppleEventBeat::requestSynchronous(std::optional surfaceId) const { - EventBeat::requestSynchronous(); + EventBeat::requestSynchronous(surfaceId); // The run loop observer that ordinarily induces the beat runs before Core // Animation commits the frame. A synchronous request made while Core @@ -167,8 +148,8 @@ void schedule() const // Scheduling an induce in the display phase of the current commit cycle // processes it before this frame is presented. Multiple requests within one // cycle coalesce into a single induce. - if (RCTIsMainQueue()) { - displayPhaseFlusher_->schedule(); + if (surfaceId.has_value() && RCTIsMainQueue()) { + displayPhaseFlusher_->schedule(*surfaceId); } } diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 0e4bbe376463..6035710e11d5 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -292,11 +292,18 @@ - (RCTScheduler *)_createScheduler toolbox.runtimeExecutor = runtimeExecutor; toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor; - toolbox.eventBeatFactory = - [runtimeScheduler](std::shared_ptr ownerBox) -> std::unique_ptr { + RCTSurfaceRegistry *surfaceRegistry = _surfaceRegistry; + toolbox.eventBeatFactory = [runtimeScheduler, + surfaceRegistry](std::shared_ptr ownerBox) -> std::unique_ptr { auto runLoopObserver = std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); - return std::make_unique(std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler); + // The registry is thread-safe, but the resolver is only called on the + // main thread from the display-phase flusher. + auto surfaceLayerResolver = [surfaceRegistry](SurfaceId surfaceId) -> CALayer * { + return [surfaceRegistry surfaceForRootTag:surfaceId].view.layer; + }; + return std::make_unique( + std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(surfaceLayerResolver)); }; RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox]; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp index 839e2f19f90b..aff7c248c4f0 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp @@ -25,7 +25,8 @@ void EventBeat::request() const { isEventBeatRequested_ = true; } -void EventBeat::requestSynchronous() const { +void EventBeat::requestSynchronous( + std::optional /*surfaceId*/) const { react_native_assert( beatCallback_ && "Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous."); diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h index b8da33cc9ac2..d24f0cfcb993 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -109,7 +111,13 @@ class EventBeat { * Both JS and UI thread are * blocked. */ - virtual void requestSynchronous() const; + /* + * The surface the synchronous request originates from, when known, lets + * platform implementations schedule an induce where that surface renders. + * Without it the induce follows the platform's ordinary beat timing. + */ + virtual void requestSynchronous( + std::optional surfaceId = std::nullopt) const; /* * Induces the next beat to happen as soon as possible. diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp index 5fa5e6821a51..3c97aac5fb10 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp @@ -37,8 +37,9 @@ void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const { eventQueue_.enqueueEvent(std::move(rawEvent)); } -void EventDispatcher::experimental_flushSync() const { - eventQueue_.experimental_flushSync(); +void EventDispatcher::experimental_flushSync( + std::optional surfaceId) const { + eventQueue_.experimental_flushSync(surfaceId); } void EventDispatcher::dispatchStateUpdate( diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h index 88a9a953e423..def93019970c 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -44,7 +46,7 @@ class EventDispatcher { /* * Experimental API exposed to support EventEmitter::experimental_flushSync. */ - void experimental_flushSync() const; + void experimental_flushSync(std::optional surfaceId) const; /* * Dispatches a raw event with asynchronous batched priority. Before the diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp index 2d6ac50e3730..c6402bc52a52 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp @@ -7,6 +7,8 @@ #include "EventEmitter.h" +#include + #include #include #include @@ -229,6 +231,14 @@ void EventEmitter::setEnabled(bool enabled) { } } +std::optional EventEmitter::getSurfaceId() const { + std::scoped_lock lock(DispatchMutex()); + if (auto shadowNodeFamily = shadowNodeFamily_.lock()) { + return shadowNodeFamily->getSurfaceId(); + } + return std::nullopt; +} + void EventEmitter::setShadowNodeFamily( std::weak_ptr shadowNodeFamily) { shadowNodeFamily_ = std::move(shadowNodeFamily); diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h index f3e9a4c334e7..0297477a4ce1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -63,6 +64,11 @@ class EventEmitter { const SharedEventTarget &getEventTarget() const; + /* + * The surface of the corresponding ShadowNodeFamily, when one is attached. + */ + std::optional getSurfaceId() const; + /* * Experimental API that will change in the future. */ @@ -74,8 +80,10 @@ class EventEmitter { return; } + auto surfaceId = getSurfaceId(); + syncFunc(); - eventDispatcher->experimental_flushSync(); + eventDispatcher->experimental_flushSync(surfaceId); } /* diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp index 6e99fd71bb7a..ef5cd7c986f6 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp @@ -86,8 +86,9 @@ void EventQueue::onEnqueue() const { eventBeat_->request(); } -void EventQueue::experimental_flushSync() const { - eventBeat_->requestSynchronous(); +void EventQueue::experimental_flushSync( + std::optional surfaceId) const { + eventBeat_->requestSynchronous(surfaceId); } void EventQueue::onBeat(jsi::Runtime& runtime) const { diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h index f525767478aa..f399ec8f58bb 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -61,7 +63,7 @@ class EventQueue { /* * Experimental API exposed to support EventEmitter::experimental_flushSync. */ - void experimental_flushSync() const; + void experimental_flushSync(std::optional surfaceId) const; protected: /*