Skip to content

Commit ad3233e

Browse files
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.
1 parent dd5c6d7 commit ad3233e

11 files changed

Lines changed: 113 additions & 79 deletions

File tree

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
#pragma once
99

10+
#include <functional>
1011
#include <memory>
12+
#include <optional>
13+
14+
#import <QuartzCore/QuartzCore.h>
1115

1216
#include <ReactCommon/RuntimeExecutor.h>
1317
#include <react/renderer/core/EventBeat.h>
@@ -25,18 +29,27 @@ class RuntimeScheduler;
2529
* A synchronous request made while Core Animation is laying out the current
2630
* frame (the run loop observer that induces the beat has already run at that
2731
* point) is additionally induced from the display phase of the same commit
28-
* cycle, so that its effects are mounted before the frame is presented.
32+
* cycle, so that its effects are mounted before the frame is presented. The
33+
* induce is scheduled on the layer of the requesting surface's root view —
34+
* the tree Core Animation is laying out when the request is made from layout.
2935
*/
3036
class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate {
3137
public:
38+
/*
39+
* Resolves the layer of a surface's root view. Called on the main thread;
40+
* returns nil when the surface is unknown or its view is not mounted.
41+
*/
42+
using SurfaceLayerResolver = std::function<CALayer *(SurfaceId)>;
43+
3244
AppleEventBeat(
3345
std::shared_ptr<OwnerBox> ownerBox,
3446
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
35-
RuntimeScheduler &RuntimeScheduler);
47+
RuntimeScheduler &RuntimeScheduler,
48+
SurfaceLayerResolver surfaceLayerResolver);
3649

3750
~AppleEventBeat() override;
3851

39-
void requestSynchronous() const override;
52+
void requestSynchronous(std::optional<SurfaceId> surfaceId) const override;
4053

4154
#pragma mark - RunLoopObserver::Delegate
4255

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

Lines changed: 45 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -40,49 +40,25 @@ - (void)display
4040

4141
@end
4242

43-
/*
44-
* The windows that can commit a Core Animation transaction: the visible ones
45-
* of every foreground scene.
46-
*/
47-
static NSArray<UIWindow *> *RCTFlushableWindows(void)
48-
{
49-
NSMutableArray<UIWindow *> *windows = [NSMutableArray new];
50-
for (UIScene *scene in RCTSharedApplication().connectedScenes) {
51-
if (![scene isKindOfClass:[UIWindowScene class]]) {
52-
continue;
53-
}
54-
if (scene.activationState != UISceneActivationStateForegroundActive &&
55-
scene.activationState != UISceneActivationStateForegroundInactive) {
56-
continue;
57-
}
58-
for (UIWindow *window in ((UIWindowScene *)scene).windows) {
59-
if (!window.hidden) {
60-
[windows addObject:window];
61-
}
62-
}
63-
}
64-
if (windows.count == 0) {
65-
// Apps on the legacy UIApplicationDelegate lifecycle own their window
66-
// outside of any scene, so the enumeration above finds nothing.
67-
UIWindow *keyWindow = RCTKeyWindow();
68-
if (keyWindow != nil) {
69-
[windows addObject:keyWindow];
70-
}
71-
}
72-
return windows;
73-
}
74-
7543
namespace facebook::react {
7644

7745
/*
78-
* Owns the flusher layers and keeps one attached to every window's layer so
79-
* that whichever layer tree is being committed contains one of them.
46+
* Owns the flusher layers, one per surface that has made a synchronous
47+
* request, attached to the layer of that surface's root view. A request made
48+
* from layout runs inside the commit of exactly that tree, so its layer is
49+
* guaranteed a display phase in the current cycle — no assumption about
50+
* which window is key or about all windows committing together.
8051
*/
8152
class AppleEventBeat::DisplayPhaseFlusher {
8253
public:
83-
DisplayPhaseFlusher(std::function<void()> callback, std::weak_ptr<const void> weakOwner)
54+
DisplayPhaseFlusher(
55+
std::function<void()> callback,
56+
std::weak_ptr<const void> weakOwner,
57+
SurfaceLayerResolver surfaceLayerResolver)
58+
: surfaceLayerResolver_(std::move(surfaceLayerResolver))
8459
{
85-
// Weak keys: a window that goes away takes its own layer with it.
60+
// Weak keys: a root view that goes away takes its own flusher layer with
61+
// it.
8662
layers_ = [NSMapTable weakToStrongObjectsMapTable];
8763
auto sharedCallback = std::make_shared<std::function<void()>>(std::move(callback));
8864
onDisplay_ = ^{
@@ -101,7 +77,7 @@ - (void)display
10177
// The beat can be destroyed on any thread; layer mutations belong on the
10278
// main thread. The block only retains the layers, and a display happening
10379
// before this executes is made safe by the owner check above.
104-
NSMapTable<UIWindow *, RCTEventBeatFlusherLayer *> *layers = layers_;
80+
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers = layers_;
10581
RCTExecuteOnMainQueue(^{
10682
for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) {
10783
layer.onDisplay = nil;
@@ -113,52 +89,57 @@ - (void)display
11389

11490
/*
11591
* Schedules the callback to run in the display phase of the current (or
116-
* next) Core Animation commit cycle. Main thread only.
117-
*
118-
* Every window gets a layer rather than only the key window: the request can
119-
* come from any of them — a modal and the LogBox are windows of their own —
120-
* and only a layer in a tree that is committed is displayed in this cycle.
121-
* The induce the display triggers is coalescing, so the extra layers cost a
122-
* dirty zero-sized layer each, not extra beats.
92+
* next) Core Animation commit cycle, on the layer tree of the surface's
93+
* root view. Main thread only. Does nothing when the surface has no mounted
94+
* view; the run loop observer then processes the request on its ordinary
95+
* schedule instead.
12396
*/
124-
void schedule() const
97+
void schedule(SurfaceId surfaceId) const
12598
{
126-
for (UIWindow *window in RCTFlushableWindows()) {
127-
RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:window];
128-
if (layer == nil) {
129-
layer = [RCTEventBeatFlusherLayer new];
130-
layer.frame = CGRectZero;
131-
layer.onDisplay = onDisplay_;
132-
[layers_ setObject:layer forKey:window];
133-
}
134-
if (layer.superlayer != window.layer) {
135-
[window.layer addSublayer:layer];
136-
}
137-
[layer setNeedsDisplay];
99+
CALayer *hostLayer = surfaceLayerResolver_ ? surfaceLayerResolver_(surfaceId) : nil;
100+
if (hostLayer == nil) {
101+
return;
102+
}
103+
RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:hostLayer];
104+
if (layer == nil) {
105+
layer = [RCTEventBeatFlusherLayer new];
106+
layer.frame = CGRectZero;
107+
layer.onDisplay = onDisplay_;
108+
[layers_ setObject:layer forKey:hostLayer];
109+
}
110+
if (layer.superlayer != hostLayer) {
111+
[layer removeFromSuperlayer];
112+
[hostLayer addSublayer:layer];
138113
}
114+
[layer setNeedsDisplay];
139115
}
140116

141117
private:
142-
NSMapTable<UIWindow *, RCTEventBeatFlusherLayer *> *layers_;
118+
SurfaceLayerResolver surfaceLayerResolver_;
119+
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers_;
143120
void (^onDisplay_)(void);
144121
};
145122

146123
AppleEventBeat::AppleEventBeat(std::shared_ptr<OwnerBox> ownerBox,
147124
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
148-
RuntimeScheduler &runtimeScheduler)
125+
RuntimeScheduler &runtimeScheduler,
126+
SurfaceLayerResolver surfaceLayerResolver)
149127
: EventBeat(std::move(ownerBox), runtimeScheduler),
150128
uiRunLoopObserver_(std::move(uiRunLoopObserver)),
151-
displayPhaseFlusher_(std::make_unique<DisplayPhaseFlusher>([this]() { induce(); }, ownerBox_->owner))
129+
displayPhaseFlusher_(std::make_unique<DisplayPhaseFlusher>(
130+
[this]() { induce(); },
131+
ownerBox_->owner,
132+
std::move(surfaceLayerResolver)))
152133
{
153134
uiRunLoopObserver_->setDelegate(this);
154135
uiRunLoopObserver_->enable();
155136
}
156137

157138
AppleEventBeat::~AppleEventBeat() = default;
158139

159-
void AppleEventBeat::requestSynchronous() const
140+
void AppleEventBeat::requestSynchronous(std::optional<SurfaceId> surfaceId) const
160141
{
161-
EventBeat::requestSynchronous();
142+
EventBeat::requestSynchronous(surfaceId);
162143

163144
// The run loop observer that ordinarily induces the beat runs before Core
164145
// Animation commits the frame. A synchronous request made while Core
@@ -167,8 +148,8 @@ void schedule() const
167148
// Scheduling an induce in the display phase of the current commit cycle
168149
// processes it before this frame is presented. Multiple requests within one
169150
// cycle coalesce into a single induce.
170-
if (RCTIsMainQueue()) {
171-
displayPhaseFlusher_->schedule();
151+
if (surfaceId.has_value() && RCTIsMainQueue()) {
152+
displayPhaseFlusher_->schedule(*surfaceId);
172153
}
173154
}
174155

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

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

295-
toolbox.eventBeatFactory =
296-
[runtimeScheduler](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
295+
RCTSurfaceRegistry *surfaceRegistry = _surfaceRegistry;
296+
toolbox.eventBeatFactory = [runtimeScheduler,
297+
surfaceRegistry](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+
// The registry is thread-safe, but the resolver is only called on the
301+
// main thread from the display-phase flusher.
302+
auto surfaceLayerResolver = [surfaceRegistry](SurfaceId surfaceId) -> CALayer * {
303+
return [surfaceRegistry surfaceForRootTag:surfaceId].view.layer;
304+
};
305+
return std::make_unique<AppleEventBeat>(
306+
std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(surfaceLayerResolver));
300307
};
301308

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

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ void EventBeat::request() const {
2525
isEventBeatRequested_ = true;
2626
}
2727

28-
void EventBeat::requestSynchronous() const {
28+
void EventBeat::requestSynchronous(
29+
std::optional<SurfaceId> /*surfaceId*/) const {
2930
react_native_assert(
3031
beatCallback_ &&
3132
"Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous.");

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

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

88
#pragma once
99

10+
#include <optional>
11+
#include <react/renderer/core/ReactPrimitives.h>
1012
#include <atomic>
1113
#include <functional>
1214
#include <memory>
@@ -109,7 +111,13 @@ class EventBeat {
109111
* Both JS and UI thread are
110112
* blocked.
111113
*/
112-
virtual void requestSynchronous() const;
114+
/*
115+
* The surface the synchronous request originates from, when known, lets
116+
* platform implementations schedule an induce where that surface renders.
117+
* Without it the induce follows the platform's ordinary beat timing.
118+
*/
119+
virtual void requestSynchronous(
120+
std::optional<SurfaceId> surfaceId = std::nullopt) const;
113121

114122
/*
115123
* Induces the next beat to happen as soon as possible.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ 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(
41+
std::optional<SurfaceId> surfaceId) const {
42+
eventQueue_.experimental_flushSync(surfaceId);
4243
}
4344

4445
void EventDispatcher::dispatchStateUpdate(

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#pragma once
99

10+
#include <optional>
11+
#include <react/renderer/core/ReactPrimitives.h>
1012
#include <react/renderer/core/EventBeat.h>
1113
#include <react/renderer/core/EventListener.h>
1214
#include <react/renderer/core/EventLogger.h>
@@ -44,7 +46,7 @@ class EventDispatcher {
4446
/*
4547
* Experimental API exposed to support EventEmitter::experimental_flushSync.
4648
*/
47-
void experimental_flushSync() const;
49+
void experimental_flushSync(std::optional<SurfaceId> surfaceId) const;
4850

4951
/*
5052
* Dispatches a raw event with asynchronous batched priority. Before the

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

Lines changed: 10 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>
@@ -229,6 +231,14 @@ void EventEmitter::setEnabled(bool enabled) {
229231
}
230232
}
231233

234+
std::optional<SurfaceId> EventEmitter::getSurfaceId() const {
235+
std::scoped_lock lock(DispatchMutex());
236+
if (auto shadowNodeFamily = shadowNodeFamily_.lock()) {
237+
return shadowNodeFamily->getSurfaceId();
238+
}
239+
return std::nullopt;
240+
}
241+
232242
void EventEmitter::setShadowNodeFamily(
233243
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily) {
234244
shadowNodeFamily_ = std::move(shadowNodeFamily);

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

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

88
#pragma once
99

10+
#include <optional>
1011
#include <memory>
1112
#include <mutex>
1213

@@ -63,6 +64,11 @@ class EventEmitter {
6364

6465
const SharedEventTarget &getEventTarget() const;
6566

67+
/*
68+
* The surface of the corresponding ShadowNodeFamily, when one is attached.
69+
*/
70+
std::optional<SurfaceId> getSurfaceId() const;
71+
6672
/*
6773
* Experimental API that will change in the future.
6874
*/
@@ -74,8 +80,10 @@ class EventEmitter {
7480
return;
7581
}
7682

83+
auto surfaceId = getSurfaceId();
84+
7785
syncFunc();
78-
eventDispatcher->experimental_flushSync();
86+
eventDispatcher->experimental_flushSync(surfaceId);
7987
}
8088

8189
/*

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ void EventQueue::onEnqueue() const {
8686
eventBeat_->request();
8787
}
8888

89-
void EventQueue::experimental_flushSync() const {
90-
eventBeat_->requestSynchronous();
89+
void EventQueue::experimental_flushSync(
90+
std::optional<SurfaceId> surfaceId) const {
91+
eventBeat_->requestSynchronous(surfaceId);
9192
}
9293

9394
void EventQueue::onBeat(jsi::Runtime& runtime) const {

0 commit comments

Comments
 (0)