Skip to content
Open
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.

34 changes: 33 additions & 1 deletion packages/react-native/React/Fabric/AppleEventBeat.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@

#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>

@class RCTEventBeatFlusherLayer;

namespace facebook::react {

class RuntimeScheduler;
Expand All @@ -19,13 +27,34 @@ 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 view's window — the root
* of 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 the window containing the view with the given tag.
* Called on the main thread; returns nil when the view is not mounted or
* not attached to a window.
*/
using WindowLayerResolver = std::function<CALayer *(Tag)>;

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

~AppleEventBeat() override;

using EventBeat::requestSynchronous;
void requestSynchronous(Tag tag) const override;

#pragma mark - RunLoopObserver::Delegate

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

private:
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver_;
WindowLayerResolver windowLayerResolver_;
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers_;
void (^onDisplay_)(void);
};

} // namespace facebook::react
124 changes: 124 additions & 0 deletions packages/react-native/React/Fabric/AppleEventBeat.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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
- (instancetype)initWithOnDisplay:(void (^)(void))onDisplay;
@end

@implementation RCTEventBeatFlusherLayer {
void (^_onDisplay)(void);
}

- (instancetype)initWithOnDisplay:(void (^)(void))onDisplay
{
if (self = [super init]) {
_onDisplay = [onDisplay copy];
self.frame = CGRectZero;
}
return self;
}

- (void)display
{
_onDisplay();
}

// The layer is not a visual element; never participate in animations.
- (id<CAAction>)actionForKey:(NSString *)event
{
return nil;
}

@end

namespace facebook::react {

AppleEventBeat::AppleEventBeat(
std::shared_ptr<OwnerBox> ownerBox,
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
RuntimeScheduler &runtimeScheduler,
WindowLayerResolver windowLayerResolver)
: EventBeat(std::move(ownerBox), runtimeScheduler),
uiRunLoopObserver_(std::move(uiRunLoopObserver)),
windowLayerResolver_(std::move(windowLayerResolver)),
layers_([NSMapTable weakToStrongObjectsMapTable])
{
std::weak_ptr<const void> weakOwner = ownerBox_->owner;
onDisplay_ = ^{
// The owner (indirectly) retains the event beat; if it is gone, so is
// the beat this induces.
auto owner = weakOwner.lock();
if (!owner) {
return;
}
this->induce();
};

uiRunLoopObserver_->setDelegate(this);
uiRunLoopObserver_->enable();
}

AppleEventBeat::~AppleEventBeat()
{
// 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 it executes is made safe by the owner check above.
NSMapTable<CALayer *, RCTEventBeatFlusherLayer *> *layers = layers_;
RCTExecuteOnMainQueue(^{
for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) {
[layer removeFromSuperlayer];
}
[layers removeAllObjects];
});
}

void AppleEventBeat::requestSynchronous(Tag tag) const
{
EventBeat::requestSynchronous(tag);

if (tag == kNoTag || !RCTIsMainQueue()) {
return;
}
CALayer *hostLayer = windowLayerResolver_ ? windowLayerResolver_(tag) : nil;
if (hostLayer == nil) {
return;
}
RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:hostLayer];
if (layer == nil) {
layer = [[RCTEventBeatFlusherLayer alloc] initWithOnDisplay:onDisplay_];
[layers_ setObject:layer forKey:hostLayer];
}
if (layer.superlayer != hostLayer) {
[layer removeFromSuperlayer];
[hostLayer addSublayer:layer];
}
[layer setNeedsDisplay];
}

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

} // namespace facebook::react
9 changes: 7 additions & 2 deletions packages/react-native/React/Fabric/RCTSurfacePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,16 @@ - (RCTScheduler *)_createScheduler
toolbox.runtimeExecutor = runtimeExecutor;
toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor;

RCTMountingManager *mountingManager = _mountingManager;
toolbox.eventBeatFactory =
[runtimeScheduler](std::shared_ptr<EventBeat::OwnerBox> ownerBox) -> std::unique_ptr<EventBeat> {
[runtimeScheduler, mountingManager](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);
auto windowLayerResolver = [mountingManager](Tag tag) -> CALayer * {
return [mountingManager.componentViewRegistry findComponentViewWithTag:tag].window.layer;
};
return std::make_unique<AppleEventBeat>(
std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(windowLayerResolver));
};

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

void EventBeat::requestSynchronous() const {
requestSynchronous(kNoTag);
}

void EventBeat::requestSynchronous(Tag /*tag*/) const {
react_native_assert(
beatCallback_ &&
"Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous.");
Expand Down Expand Up @@ -53,7 +57,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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <react/cxxstableapi/UmbrellaGuard.h>

#include <react/renderer/core/ReactPrimitives.h>
#include <atomic>
#include <functional>
#include <memory>
Expand Down Expand Up @@ -110,8 +111,17 @@ class EventBeat {
* thread────────────────────┴─────────────────────────┴▶
* Both JS and UI thread are
* blocked.
*
* `tag` is the view the request originates from, or `kNoTag` when unknown.
* Platform implementations use it to schedule an induce where that view
* renders, and fall back to their ordinary beat timing without it.
*/
virtual void requestSynchronous(Tag tag) const;

/*
* Convenience for requesters with no view attribution.
*/
virtual void requestSynchronous() const;
void requestSynchronous() const;

/*
* The callback will be executed once a consumer (for example EventQueue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const {
eventQueue_.enqueueEvent(std::move(rawEvent));
}

void EventDispatcher::experimental_flushSync() const {
eventQueue_.experimental_flushSync();
void EventDispatcher::experimental_flushSync(Tag tag) const {
eventQueue_.experimental_flushSync(tag);
}

void EventDispatcher::dispatchStateUpdate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <react/renderer/core/EventLogger.h>
#include <react/renderer/core/EventQueue.h>
#include <react/renderer/core/EventQueueProcessor.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/StatePipe.h>
#include <react/renderer/core/StateUpdate.h>
#include <memory>
Expand Down Expand Up @@ -46,7 +47,7 @@ class EventDispatcher {
/*
* Experimental API exposed to support EventEmitter::experimental_flushSync.
*/
void experimental_flushSync() const;
void experimental_flushSync(Tag tag) 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 @@ -231,6 +233,9 @@ void EventEmitter::setEnabled(bool enabled) {

void EventEmitter::setShadowNodeFamily(
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily) {
if (auto family = shadowNodeFamily.lock()) {
tag_ = family->getTag();
}
shadowNodeFamily_ = std::move(shadowNodeFamily);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class EventEmitter {
}

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

/*
Expand Down Expand Up @@ -134,6 +134,7 @@ class EventEmitter {
friend class UIManagerBinding;

SharedEventTarget eventTarget_;
Tag tag_{kNoTag};
std::weak_ptr<const ShadowNodeFamily> shadowNodeFamily_;

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

void EventQueue::experimental_flushSync() const {
eventBeat_->requestSynchronous();
void EventQueue::experimental_flushSync(Tag tag) const {
eventBeat_->requestSynchronous(tag);
}

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

#include <react/cxxstableapi/UmbrellaGuard.h>

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

protected:
/*
Expand Down
Loading
Loading