Skip to content

Commit d804f43

Browse files
Process synchronous event beats in the frame that requested them
EventEmitter::experimental_flushSync only requests a beat, processed at the next EventBeat::induce. On iOS 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 additionally 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. The layer is attached to the window of the requesting view: experimental_flushSync carries the tag of the emitting view — cached from its ShadowNodeFamily when the family is attached at creation, before the emitter is published, so reading it takes no lock — through EventDispatcher and EventQueue to EventBeat::requestSynchronous, with kNoTag meaning no view attribution; a no-argument overload keeps unattributed requesters unchanged. AppleEventBeat resolves the tag to the view's window layer through a resolver injected by RCTSurfacePresenter (findComponentViewWithTag: on the mounting registry, a nullable, non-creating, main-thread lookup). The requesting view's window is by definition the root of the layer tree whose layout emitted the request, so the flusher is guaranteed a display phase in the current commit cycle, including for content UIKit mounts in a window of its own, like a full screen modal or LogBox. Requests from several windows in one cycle each dirty their own layer; the first display to fire drains the queue and the rest no-op on the request flag. VirtualView's synchronous flushes get the same targeting through their own emitter. A related fix 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). AppleEventBeat.cpp becomes .mm for the Objective-C. Covered by new unit tests in EventBeatTest.cpp, which drive the protected induce through a subclass standing in for the platform. The C++ API snapshots are regenerated; the deltas are the requestSynchronous overload pair, the resolver type, and the AppleEventBeat constructor and destructor.
1 parent 7e02fc7 commit d804f43

22 files changed

Lines changed: 420 additions & 55 deletions

packages/react-native/React/Fabric/AppleEventBeat.cpp

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/react-native/React/Fabric/AppleEventBeat.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77

88
#pragma once
99

10+
#include <functional>
11+
#include <memory>
12+
#include <optional>
13+
14+
#import <QuartzCore/QuartzCore.h>
15+
1016
#include <ReactCommon/RuntimeExecutor.h>
1117
#include <react/renderer/core/EventBeat.h>
1218
#include <react/utils/RunLoopObserver.h>
1319

20+
@class RCTEventBeatFlusherLayer;
21+
1422
namespace facebook::react {
1523

1624
class RuntimeScheduler;
@@ -19,13 +27,34 @@ class RuntimeScheduler;
1927
* Event beat associated with JavaScript runtime.
2028
* The beat is called on `RuntimeExecutor`'s thread induced by the UI thread
2129
* event loop.
30+
*
31+
* A synchronous request made while Core Animation is laying out the current
32+
* frame (the run loop observer that induces the beat has already run at that
33+
* point) is additionally induced from the display phase of the same commit
34+
* cycle, so that its effects are mounted before the frame is presented. The
35+
* induce is scheduled on the layer of the requesting view's window — the root
36+
* of the tree Core Animation is laying out when the request is made from
37+
* layout.
2238
*/
2339
class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate {
2440
public:
41+
/*
42+
* Resolves the layer of the window containing the view with the given tag.
43+
* Called on the main thread; returns nil when the view is not mounted or
44+
* not attached to a window.
45+
*/
46+
using WindowLayerResolver = std::function<CALayer *(Tag)>;
47+
2548
AppleEventBeat(
2649
std::shared_ptr<OwnerBox> ownerBox,
2750
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
28-
RuntimeScheduler &RuntimeScheduler);
51+
RuntimeScheduler &RuntimeScheduler,
52+
WindowLayerResolver windowLayerResolver);
53+
54+
~AppleEventBeat() override;
55+
56+
using EventBeat::requestSynchronous;
57+
void requestSynchronous(Tag tag) const override;
2958

3059
#pragma mark - RunLoopObserver::Delegate
3160

@@ -34,6 +63,9 @@ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate {
3463

3564
private:
3665
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver_;
66+
WindowLayerResolver windowLayerResolver_;
67+
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers_;
68+
void (^onDisplay_)(void);
3769
};
3870

3971
} // namespace facebook::react
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "AppleEventBeat.h"
9+
10+
#import <QuartzCore/QuartzCore.h>
11+
#import <React/RCTUtils.h>
12+
13+
#include <react/debug/react_native_assert.h>
14+
15+
/*
16+
* A zero-sized layer whose only purpose is to run a callback during the
17+
* display phase of a Core Animation commit. Core Animation processes a commit
18+
* as layout → display → (repeat until stable) → commit, so a layer marked as
19+
* needing display during the layout phase has its `display` called after the
20+
* whole layout pass but before the transaction is committed.
21+
*/
22+
@interface RCTEventBeatFlusherLayer : CALayer
23+
- (instancetype)initWithOnDisplay:(void (^)(void))onDisplay;
24+
@end
25+
26+
@implementation RCTEventBeatFlusherLayer {
27+
void (^_onDisplay)(void);
28+
}
29+
30+
- (instancetype)initWithOnDisplay:(void (^)(void))onDisplay
31+
{
32+
if (self = [super init]) {
33+
_onDisplay = [onDisplay copy];
34+
self.frame = CGRectZero;
35+
}
36+
return self;
37+
}
38+
39+
- (void)display
40+
{
41+
_onDisplay();
42+
}
43+
44+
// The layer is not a visual element; never participate in animations.
45+
- (id<CAAction>)actionForKey:(NSString *)event
46+
{
47+
return nil;
48+
}
49+
50+
@end
51+
52+
namespace facebook::react {
53+
54+
AppleEventBeat::AppleEventBeat(
55+
std::shared_ptr<OwnerBox> ownerBox,
56+
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
57+
RuntimeScheduler &runtimeScheduler,
58+
WindowLayerResolver windowLayerResolver)
59+
: EventBeat(std::move(ownerBox), runtimeScheduler),
60+
uiRunLoopObserver_(std::move(uiRunLoopObserver)),
61+
windowLayerResolver_(std::move(windowLayerResolver)),
62+
layers_([NSMapTable weakToStrongObjectsMapTable])
63+
{
64+
std::weak_ptr<const void> weakOwner = ownerBox_->owner;
65+
onDisplay_ = ^{
66+
// The owner (indirectly) retains the event beat; if it is gone, so is
67+
// the beat this induces.
68+
auto owner = weakOwner.lock();
69+
if (!owner) {
70+
return;
71+
}
72+
this->induce();
73+
};
74+
75+
uiRunLoopObserver_->setDelegate(this);
76+
uiRunLoopObserver_->enable();
77+
}
78+
79+
AppleEventBeat::~AppleEventBeat()
80+
{
81+
// The beat can be destroyed on any thread; layer mutations belong on the
82+
// main thread. The block only retains the layers, and a display happening
83+
// before it executes is made safe by the owner check above.
84+
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers = layers_;
85+
RCTExecuteOnMainQueue(^{
86+
for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) {
87+
[layer removeFromSuperlayer];
88+
}
89+
[layers removeAllObjects];
90+
});
91+
}
92+
93+
void AppleEventBeat::requestSynchronous(Tag tag) const
94+
{
95+
EventBeat::requestSynchronous(tag);
96+
97+
if (tag == kNoTag || !RCTIsMainQueue()) {
98+
return;
99+
}
100+
CALayer *hostLayer = windowLayerResolver_ ? windowLayerResolver_(tag) : nil;
101+
if (hostLayer == nil) {
102+
return;
103+
}
104+
RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:hostLayer];
105+
if (layer == nil) {
106+
layer = [[RCTEventBeatFlusherLayer alloc] initWithOnDisplay:onDisplay_];
107+
[layers_ setObject:layer forKey:hostLayer];
108+
}
109+
if (layer.superlayer != hostLayer) {
110+
[layer removeFromSuperlayer];
111+
[hostLayer addSublayer:layer];
112+
}
113+
[layer setNeedsDisplay];
114+
}
115+
116+
void AppleEventBeat::activityDidChange(
117+
const RunLoopObserver::Delegate *delegate,
118+
RunLoopObserver::Activity /*activity*/) const noexcept
119+
{
120+
react_native_assert(delegate == this);
121+
induce();
122+
}
123+
124+
} // namespace facebook::react

packages/react-native/React/Fabric/RCTSurfacePresenter.mm

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,16 @@ - (RCTScheduler *)_createScheduler
292292
toolbox.runtimeExecutor = runtimeExecutor;
293293
toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor;
294294

295+
RCTMountingManager *mountingManager = _mountingManager;
295296
toolbox.eventBeatFactory =
296-
[runtimeScheduler](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
297+
[runtimeScheduler, mountingManager](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
297298
auto runLoopObserver =
298299
std::make_unique<const MainRunLoopObserver>(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner);
299-
return std::make_unique<AppleEventBeat>(std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler);
300+
auto windowLayerResolver = [mountingManager](Tag tag) -> CALayer * {
301+
return [mountingManager.componentViewRegistry findComponentViewWithTag:tag].window.layer;
302+
};
303+
return std::make_unique<AppleEventBeat>(
304+
std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(windowLayerResolver));
300305
};
301306

302307
RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox];

packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ void EventBeat::request() const {
2626
}
2727

2828
void EventBeat::requestSynchronous() const {
29+
requestSynchronous(kNoTag);
30+
}
31+
32+
void EventBeat::requestSynchronous(Tag /*tag*/) const {
2933
react_native_assert(
3034
beatCallback_ &&
3135
"Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous.");
@@ -53,7 +57,14 @@ void EventBeat::induce() const {
5357
isEventBeatRequested_ = false;
5458

5559
if (isBeatCallbackScheduled_) {
56-
return;
60+
// An asynchronous beat is already scheduled but has not run yet. A
61+
// synchronous request must not be stranded behind it (it would silently
62+
// lose its this-frame guarantee, and the leftover flag would make an
63+
// unrelated later beat blocking), so it proceeds and processes the queue
64+
// now; the already scheduled beat will simply find an empty queue.
65+
if (!isSynchronousRequested_) {
66+
return;
67+
}
5768
}
5869

5970
isBeatCallbackScheduled_ = true;

packages/react-native/ReactCommon/react/renderer/core/EventBeat.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <react/cxxstableapi/UmbrellaGuard.h>
1111

12+
#include <react/renderer/core/ReactPrimitives.h>
1213
#include <atomic>
1314
#include <functional>
1415
#include <memory>
@@ -110,8 +111,17 @@ class EventBeat {
110111
* thread────────────────────┴─────────────────────────┴▶
111112
* Both JS and UI thread are
112113
* blocked.
114+
*
115+
* `tag` is the view the request originates from, or `kNoTag` when unknown.
116+
* Platform implementations use it to schedule an induce where that view
117+
* renders, and fall back to their ordinary beat timing without it.
118+
*/
119+
virtual void requestSynchronous(Tag tag) const;
120+
121+
/*
122+
* Convenience for requesters with no view attribution.
113123
*/
114-
virtual void requestSynchronous() const;
124+
void requestSynchronous() const;
115125

116126
/*
117127
* The callback will be executed once a consumer (for example EventQueue)

packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const {
3737
eventQueue_.enqueueEvent(std::move(rawEvent));
3838
}
3939

40-
void EventDispatcher::experimental_flushSync() const {
41-
eventQueue_.experimental_flushSync();
40+
void EventDispatcher::experimental_flushSync(Tag tag) const {
41+
eventQueue_.experimental_flushSync(tag);
4242
}
4343

4444
void EventDispatcher::dispatchStateUpdate(

packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <react/renderer/core/EventLogger.h>
1515
#include <react/renderer/core/EventQueue.h>
1616
#include <react/renderer/core/EventQueueProcessor.h>
17+
#include <react/renderer/core/ReactPrimitives.h>
1718
#include <react/renderer/core/StatePipe.h>
1819
#include <react/renderer/core/StateUpdate.h>
1920
#include <memory>
@@ -46,7 +47,7 @@ class EventDispatcher {
4647
/*
4748
* Experimental API exposed to support EventEmitter::experimental_flushSync.
4849
*/
49-
void experimental_flushSync() const;
50+
void experimental_flushSync(Tag tag) const;
5051

5152
/*
5253
* Dispatches a raw event with asynchronous batched priority. Before the

packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#include "EventEmitter.h"
99

10+
#include <react/renderer/core/ShadowNodeFamily.h>
11+
1012
#include <cxxreact/TraceSection.h>
1113
#include <folly/dynamic.h>
1214
#include <jsi/jsi.h>
@@ -231,6 +233,9 @@ void EventEmitter::setEnabled(bool enabled) {
231233

232234
void EventEmitter::setShadowNodeFamily(
233235
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily) {
236+
if (auto family = shadowNodeFamily.lock()) {
237+
tag_ = family->getTag();
238+
}
234239
shadowNodeFamily_ = std::move(shadowNodeFamily);
235240
}
236241

packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class EventEmitter {
7777
}
7878

7979
syncFunc();
80-
eventDispatcher->experimental_flushSync();
80+
eventDispatcher->experimental_flushSync(tag_);
8181
}
8282

8383
/*
@@ -134,6 +134,7 @@ class EventEmitter {
134134
friend class UIManagerBinding;
135135

136136
SharedEventTarget eventTarget_;
137+
Tag tag_{kNoTag};
137138
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily_;
138139

139140
EventDispatcher::Weak eventDispatcher_;

0 commit comments

Comments
 (0)