Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/react-native/React/Fabric/AppleEventBeat.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

#pragma once

#include <functional>
#include <memory>
#include <optional>

#import <QuartzCore/QuartzCore.h>

#include <ReactCommon/RuntimeExecutor.h>
#include <react/renderer/core/EventBeat.h>
Expand All @@ -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<CALayer *(SurfaceId)>;

AppleEventBeat(
std::shared_ptr<OwnerBox> ownerBox,
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
RuntimeScheduler &RuntimeScheduler);
RuntimeScheduler &RuntimeScheduler,
SurfaceLayerResolver surfaceLayerResolver);

~AppleEventBeat() override;

void requestSynchronous() const override;
void requestSynchronous(std::optional<SurfaceId> surfaceId) const override;

#pragma mark - RunLoopObserver::Delegate

Expand Down
109 changes: 45 additions & 64 deletions packages/react-native/React/Fabric/AppleEventBeat.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIWindow *> *RCTFlushableWindows(void)
{
NSMutableArray<UIWindow *> *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<void()> callback, std::weak_ptr<const void> weakOwner)
DisplayPhaseFlusher(
std::function<void()> callback,
std::weak_ptr<const void> 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::function<void()>>(std::move(callback));
onDisplay_ = ^{
Expand All @@ -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<UIWindow *, RCTEventBeatFlusherLayer *> *layers = layers_;
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers = layers_;
RCTExecuteOnMainQueue(^{
for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) {
layer.onDisplay = nil;
Expand All @@ -113,52 +89,57 @@ - (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<UIWindow *, RCTEventBeatFlusherLayer *> *layers_;
SurfaceLayerResolver surfaceLayerResolver_;
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers_;
void (^onDisplay_)(void);
};

AppleEventBeat::AppleEventBeat(std::shared_ptr<OwnerBox> ownerBox,
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
RuntimeScheduler &runtimeScheduler)
RuntimeScheduler &runtimeScheduler,
SurfaceLayerResolver surfaceLayerResolver)
: EventBeat(std::move(ownerBox), runtimeScheduler),
uiRunLoopObserver_(std::move(uiRunLoopObserver)),
displayPhaseFlusher_(std::make_unique<DisplayPhaseFlusher>([this]() { induce(); }, ownerBox_->owner))
displayPhaseFlusher_(std::make_unique<DisplayPhaseFlusher>(
[this]() { induce(); },
ownerBox_->owner,
std::move(surfaceLayerResolver)))
{
uiRunLoopObserver_->setDelegate(this);
uiRunLoopObserver_->enable();
}

AppleEventBeat::~AppleEventBeat() = default;

void AppleEventBeat::requestSynchronous() const
void AppleEventBeat::requestSynchronous(std::optional<SurfaceId> 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
Expand All @@ -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);
}
}

Expand Down
13 changes: 10 additions & 3 deletions packages/react-native/React/Fabric/RCTSurfacePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,18 @@ - (RCTScheduler *)_createScheduler
toolbox.runtimeExecutor = runtimeExecutor;
toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor;

toolbox.eventBeatFactory =
[runtimeScheduler](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
RCTSurfaceRegistry *surfaceRegistry = _surfaceRegistry;
toolbox.eventBeatFactory = [runtimeScheduler,
surfaceRegistry](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
auto runLoopObserver =
std::make_unique<const MainRunLoopObserver>(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner);
return std::make_unique<AppleEventBeat>(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<AppleEventBeat>(
std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(surfaceLayerResolver));
};

RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ void EventBeat::request() const {
isEventBeatRequested_ = true;
}

void EventBeat::requestSynchronous() const {
void EventBeat::requestSynchronous(
std::optional<SurfaceId> /*surfaceId*/) const {
react_native_assert(
beatCallback_ &&
"Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <optional>
#include <react/renderer/core/ReactPrimitives.h>
#include <atomic>
#include <functional>
#include <memory>
Expand Down Expand Up @@ -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> surfaceId = std::nullopt) const;

/*
* Induces the next beat to happen as soon as possible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> surfaceId) const {
eventQueue_.experimental_flushSync(surfaceId);
}

void EventDispatcher::dispatchStateUpdate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <optional>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/EventBeat.h>
#include <react/renderer/core/EventListener.h>
#include <react/renderer/core/EventLogger.h>
Expand Down Expand Up @@ -44,7 +46,7 @@ class EventDispatcher {
/*
* Experimental API exposed to support EventEmitter::experimental_flushSync.
*/
void experimental_flushSync() const;
void experimental_flushSync(std::optional<SurfaceId> surfaceId) const;

/*
* Dispatches a raw event with asynchronous batched priority. Before the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "EventEmitter.h"

#include <react/renderer/core/ShadowNodeFamily.h>

#include <cxxreact/TraceSection.h>
#include <folly/dynamic.h>
#include <jsi/jsi.h>
Expand Down Expand Up @@ -229,6 +231,14 @@ void EventEmitter::setEnabled(bool enabled) {
}
}

std::optional<SurfaceId> 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<const ShadowNodeFamily> shadowNodeFamily) {
shadowNodeFamily_ = std::move(shadowNodeFamily);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#pragma once

#include <optional>
#include <memory>
#include <mutex>

Expand Down Expand Up @@ -63,6 +64,11 @@ class EventEmitter {

const SharedEventTarget &getEventTarget() const;

/*
* The surface of the corresponding ShadowNodeFamily, when one is attached.
*/
std::optional<SurfaceId> getSurfaceId() const;

/*
* Experimental API that will change in the future.
*/
Expand All @@ -74,8 +80,10 @@ class EventEmitter {
return;
}

auto surfaceId = getSurfaceId();

syncFunc();
eventDispatcher->experimental_flushSync();
eventDispatcher->experimental_flushSync(surfaceId);
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ void EventQueue::onEnqueue() const {
eventBeat_->request();
}

void EventQueue::experimental_flushSync() const {
eventBeat_->requestSynchronous();
void EventQueue::experimental_flushSync(
std::optional<SurfaceId> surfaceId) const {
eventBeat_->requestSynchronous(surfaceId);
}

void EventQueue::onBeat(jsi::Runtime& runtime) const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <optional>
#include <react/renderer/core/ReactPrimitives.h>
#include <memory>
#include <mutex>
#include <vector>
Expand Down Expand Up @@ -61,7 +63,7 @@ class EventQueue {
/*
* Experimental API exposed to support EventEmitter::experimental_flushSync.
*/
void experimental_flushSync() const;
void experimental_flushSync(std::optional<SurfaceId> surfaceId) const;

protected:
/*
Expand Down