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
31 changes: 0 additions & 31 deletions packages/react-native/React/Fabric/AppleEventBeat.cpp

This file was deleted.

29 changes: 28 additions & 1 deletion packages/react-native/React/Fabric/AppleEventBeat.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

#pragma once

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

#import <QuartzCore/QuartzCore.h>

#include <ReactCommon/RuntimeExecutor.h>
#include <react/renderer/core/EventBeat.h>
#include <react/utils/RunLoopObserver.h>
Expand All @@ -19,21 +25,42 @@ 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. 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(std::optional<SurfaceId> surfaceId) const override;

#pragma mark - RunLoopObserver::Delegate

void activityDidChange(const RunLoopObserver::Delegate *delegate, RunLoopObserver::Activity activity)
const noexcept override;

private:
class DisplayPhaseFlusher;

std::unique_ptr<const RunLoopObserver> uiRunLoopObserver_;
std::unique_ptr<DisplayPhaseFlusher> displayPhaseFlusher_;
};

} // namespace facebook::react
163 changes: 163 additions & 0 deletions packages/react-native/React/Fabric/AppleEventBeat.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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 <QuartzCore/QuartzCore.h>
#import <React/RCTUtils.h>

#include <react/debug/react_native_assert.h>

/*
* 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<CAAction>)actionForKey:(NSString *)event
{
return nil;
}

@end

namespace facebook::react {

/*
* 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,
SurfaceLayerResolver surfaceLayerResolver)
: surfaceLayerResolver_(std::move(surfaceLayerResolver))
{
// 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_ = ^{
// 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<CALayer *, RCTEventBeatFlusherLayer *> *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, 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(SurfaceId surfaceId) const
{
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:
SurfaceLayerResolver surfaceLayerResolver_;
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers_;
void (^onDisplay_)(void);
};

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

AppleEventBeat::~AppleEventBeat() = default;

void AppleEventBeat::requestSynchronous(std::optional<SurfaceId> surfaceId) const
{
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
// 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 (surfaceId.has_value() && RCTIsMainQueue()) {
displayPhaseFlusher_->schedule(*surfaceId);
}
}

void AppleEventBeat::activityDidChange(const RunLoopObserver::Delegate *delegate,
RunLoopObserver::Activity /*activity*/) const noexcept
{
react_native_assert(delegate == this);
induce();
}

} // namespace facebook::react
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 Expand Up @@ -53,7 +54,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;
Expand Down
26 changes: 19 additions & 7 deletions packages/react-native/ReactCommon/react/renderer/core/EventBeat.h
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,23 @@ 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.
* 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)
Expand All @@ -128,12 +146,6 @@ class EventBeat {
void unstable_setInduceCallback(std::function<void()> 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<void()> induceCallback_;
std::shared_ptr<OwnerBox> ownerBox_;
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
Loading
Loading