mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
afec07aca2
Summary: changelog: [internal] `useLayoutEffect` has two guarantees which React Native breaks: - Layout metrics are ready. - Updates triggered inside `useLayoutEffect` are applied before paint. State between first commit and update is not shown on the screen. React Native breaks the first guarantee because it uses Background Executor. Background executor moves Yoga layout to another thread. If user core reads layout metrics in `useLayoutEffect` hook, it is a race. The information might be there, or it might not. They can even read partially update information. This diff does not affect this. We already have a way to turn off Background Executor. We haven't done this because it introduces regressions on one screen but we have a solution for that. React Native breaks the second guarantee. After Fabric's commit phase, Fabric moves to mounting the changes right away. In this diff, we queue the mounting phase and give React a chance to change what is committed to the screen. To do that, we schedule a task with user blocking priority in `RuntimeScheduler`. React, if there is an update in `useLayoutEffect`, schedules a task in the scheduler with higher priority and stops the mounting phase. We are not delaying mounting, this just gives React a chance to interrupt it. Fabric commit phase may be triggered by different mechanisms. C++ state update, surface tear down, template update (not used atm), setNativeProps, to name a few. Fabric only needs to block paint if commit originates from React. Otherwise the scheduling is wrong and we will get into undefined behaviour land. Rollout: This change is gated behind `react_fabric:block_paint_for_use_layout_effect` and will be rolled out incrementally. Reviewed By: javache Differential Revision: D43083051 fbshipit-source-id: bb494cf56a11763e38dce7ba0093c4dafdd8bf43
403 lines
14 KiB
C++
403 lines
14 KiB
C++
/*
|
|
* 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 "Scheduler.h"
|
|
|
|
#include <glog/logging.h>
|
|
#include <jsi/jsi.h>
|
|
|
|
#include <react/debug/react_native_assert.h>
|
|
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
|
|
#include <react/renderer/core/EventQueueProcessor.h>
|
|
#include <react/renderer/core/LayoutContext.h>
|
|
#include <react/renderer/debug/SystraceSection.h>
|
|
#include <react/renderer/mounting/MountingOverrideDelegate.h>
|
|
#include <react/renderer/mounting/ShadowViewMutation.h>
|
|
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
|
|
#include <react/renderer/templateprocessor/UITemplateProcessor.h>
|
|
#include <react/renderer/uimanager/UIManager.h>
|
|
#include <react/renderer/uimanager/UIManagerBinding.h>
|
|
|
|
#ifdef RN_SHADOW_TREE_INTROSPECTION
|
|
#include <react/renderer/mounting/stubs.h>
|
|
#include <iostream>
|
|
#endif
|
|
|
|
namespace facebook::react {
|
|
|
|
Scheduler::Scheduler(
|
|
SchedulerToolbox const &schedulerToolbox,
|
|
UIManagerAnimationDelegate *animationDelegate,
|
|
SchedulerDelegate *delegate) {
|
|
runtimeExecutor_ = schedulerToolbox.runtimeExecutor;
|
|
contextContainer_ = schedulerToolbox.contextContainer;
|
|
|
|
reactNativeConfig_ =
|
|
contextContainer_->at<std::shared_ptr<const ReactNativeConfig>>(
|
|
"ReactNativeConfig");
|
|
|
|
// Creating a container for future `EventDispatcher` instance.
|
|
eventDispatcher_ = std::make_shared<std::optional<EventDispatcher const>>();
|
|
|
|
auto uiManager = std::make_shared<UIManager>(
|
|
runtimeExecutor_, schedulerToolbox.backgroundExecutor, contextContainer_);
|
|
auto eventOwnerBox = std::make_shared<EventBeat::OwnerBox>();
|
|
eventOwnerBox->owner = eventDispatcher_;
|
|
|
|
auto weakRuntimeScheduler =
|
|
contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
|
|
"RuntimeScheduler");
|
|
auto runtimeScheduler = weakRuntimeScheduler.has_value()
|
|
? weakRuntimeScheduler.value().lock()
|
|
: nullptr;
|
|
|
|
auto eventPipe = [uiManager, runtimeScheduler = runtimeScheduler.get()](
|
|
jsi::Runtime &runtime,
|
|
const EventTarget *eventTarget,
|
|
const std::string &type,
|
|
ReactEventPriority priority,
|
|
const ValueFactory &payloadFactory) {
|
|
uiManager->visitBinding(
|
|
[&](UIManagerBinding const &uiManagerBinding) {
|
|
uiManagerBinding.dispatchEvent(
|
|
runtime, eventTarget, type, priority, payloadFactory);
|
|
},
|
|
runtime);
|
|
if (runtimeScheduler != nullptr) {
|
|
runtimeScheduler->callExpiredTasks(runtime);
|
|
}
|
|
};
|
|
|
|
auto statePipe = [uiManager](StateUpdate const &stateUpdate) {
|
|
uiManager->updateState(stateUpdate);
|
|
};
|
|
|
|
// Creating an `EventDispatcher` instance inside the already allocated
|
|
// container (inside the optional).
|
|
eventDispatcher_->emplace(
|
|
EventQueueProcessor(eventPipe, statePipe),
|
|
schedulerToolbox.synchronousEventBeatFactory,
|
|
schedulerToolbox.asynchronousEventBeatFactory,
|
|
eventOwnerBox);
|
|
|
|
// Casting to `std::shared_ptr<EventDispatcher const>`.
|
|
auto eventDispatcher =
|
|
EventDispatcher::Shared{eventDispatcher_, &eventDispatcher_->value()};
|
|
|
|
componentDescriptorRegistry_ = schedulerToolbox.componentRegistryFactory(
|
|
eventDispatcher, contextContainer_);
|
|
|
|
uiManager->setDelegate(this);
|
|
uiManager->setComponentDescriptorRegistry(componentDescriptorRegistry_);
|
|
|
|
auto bindingsExecutor =
|
|
schedulerToolbox.bridgelessBindingsExecutor.has_value()
|
|
? schedulerToolbox.bridgelessBindingsExecutor.value()
|
|
: runtimeExecutor_;
|
|
bindingsExecutor([uiManager](jsi::Runtime &runtime) {
|
|
UIManagerBinding::createAndInstallIfNeeded(runtime, uiManager);
|
|
});
|
|
|
|
auto componentDescriptorRegistryKey =
|
|
"ComponentDescriptorRegistry_DO_NOT_USE_PRETTY_PLEASE";
|
|
contextContainer_->erase(componentDescriptorRegistryKey);
|
|
contextContainer_->insert(
|
|
componentDescriptorRegistryKey,
|
|
std::weak_ptr<ComponentDescriptorRegistry const>(
|
|
componentDescriptorRegistry_));
|
|
|
|
delegate_ = delegate;
|
|
commitHooks_ = schedulerToolbox.commitHooks;
|
|
uiManager_ = uiManager;
|
|
|
|
for (auto const &commitHook : commitHooks_) {
|
|
uiManager->registerCommitHook(*commitHook);
|
|
}
|
|
|
|
#ifdef ANDROID
|
|
removeOutstandingSurfacesOnDestruction_ = true;
|
|
reduceDeleteCreateMutationLayoutAnimation_ = reactNativeConfig_->getBool(
|
|
"react_fabric:reduce_delete_create_mutation_layout_animation_android");
|
|
#else
|
|
removeOutstandingSurfacesOnDestruction_ = reactNativeConfig_->getBool(
|
|
"react_fabric:remove_outstanding_surfaces_on_destruction_ios");
|
|
reduceDeleteCreateMutationLayoutAnimation_ = true;
|
|
#endif
|
|
|
|
CoreFeatures::blockPaintForUseLayoutEffect = reactNativeConfig_->getBool(
|
|
"react_fabric:block_paint_for_use_layout_effect");
|
|
|
|
if (animationDelegate != nullptr) {
|
|
animationDelegate->setComponentDescriptorRegistry(
|
|
componentDescriptorRegistry_);
|
|
animationDelegate->setReduceDeleteCreateMutation(
|
|
reduceDeleteCreateMutationLayoutAnimation_);
|
|
}
|
|
uiManager_->setAnimationDelegate(animationDelegate);
|
|
}
|
|
|
|
Scheduler::~Scheduler() {
|
|
LOG(WARNING) << "Scheduler::~Scheduler() was called (address: " << this
|
|
<< ").";
|
|
|
|
for (auto const &commitHook : commitHooks_) {
|
|
uiManager_->unregisterCommitHook(*commitHook);
|
|
}
|
|
|
|
// All Surfaces must be explicitly stopped before destroying `Scheduler`.
|
|
// The idea is that `UIManager` is allowed to call `Scheduler` only if the
|
|
// corresponding `ShadowTree` instance exists.
|
|
|
|
// The thread-safety of this operation is guaranteed by this requirement.
|
|
uiManager_->setDelegate(nullptr);
|
|
uiManager_->setAnimationDelegate(nullptr);
|
|
|
|
// Then, let's verify that the requirement was satisfied.
|
|
auto surfaceIds = std::vector<SurfaceId>{};
|
|
uiManager_->getShadowTreeRegistry().enumerate(
|
|
[&surfaceIds](ShadowTree const &shadowTree, bool &) {
|
|
surfaceIds.push_back(shadowTree.getSurfaceId());
|
|
});
|
|
|
|
// TODO(T88046056): Fix Android memory leak before uncommenting changes
|
|
// react_native_assert(
|
|
// surfaceIds.empty() &&
|
|
// "Scheduler was destroyed with outstanding Surfaces.");
|
|
|
|
if (surfaceIds.empty()) {
|
|
return;
|
|
}
|
|
|
|
LOG(ERROR) << "Scheduler was destroyed with outstanding Surfaces.";
|
|
|
|
// If we are here, that means assert didn't fire which indicates that we in
|
|
// production.
|
|
|
|
// Now we have still-running surfaces, which is no good, no good.
|
|
// That's indeed a sign of a severe issue on the application layer.
|
|
// At this point, we don't have much to lose, so we are trying to unmount all
|
|
// outstanding `ShadowTree`s to prevent all stored JSI entities from
|
|
// overliving the `Scheduler`. (Unmounting `ShadowNode`s disables
|
|
// `EventEmitter`s which destroys JSI objects.)
|
|
for (auto surfaceId : surfaceIds) {
|
|
uiManager_->getShadowTreeRegistry().visit(
|
|
surfaceId,
|
|
[](ShadowTree const &shadowTree) { shadowTree.commitEmptyTree(); });
|
|
|
|
// Removing surfaces is gated because it acquires mutex waiting for commits
|
|
// in flight; in theory, it can deadlock.
|
|
if (removeOutstandingSurfacesOnDestruction_) {
|
|
uiManager_->getShadowTreeRegistry().remove(surfaceId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Scheduler::registerSurface(
|
|
SurfaceHandler const &surfaceHandler) const noexcept {
|
|
surfaceHandler.setContextContainer(getContextContainer());
|
|
surfaceHandler.setUIManager(uiManager_.get());
|
|
}
|
|
|
|
InspectorData Scheduler::getInspectorDataForInstance(
|
|
EventEmitter const &eventEmitter) const noexcept {
|
|
return executeSynchronouslyOnSameThread_CAN_DEADLOCK<InspectorData>(
|
|
runtimeExecutor_, [=](jsi::Runtime &runtime) -> InspectorData {
|
|
auto uiManagerBinding = UIManagerBinding::getBinding(runtime);
|
|
auto value = uiManagerBinding->getInspectorDataForInstance(
|
|
runtime, eventEmitter);
|
|
|
|
// TODO T97216348: avoid transforming jsi into folly::dynamic
|
|
auto dynamic = jsi::dynamicFromValue(runtime, value);
|
|
auto source = dynamic["source"];
|
|
|
|
InspectorData result = {};
|
|
result.fileName =
|
|
source["fileName"].isNull() ? "" : source["fileName"].c_str();
|
|
result.lineNumber = (int)source["lineNumber"].getDouble();
|
|
result.columnNumber = (int)source["columnNumber"].getDouble();
|
|
result.selectedIndex = (int)dynamic["selectedIndex"].getDouble();
|
|
// TODO T97216348: remove folly::dynamic from InspectorData struct
|
|
result.props = dynamic["props"];
|
|
auto hierarchy = dynamic["hierarchy"];
|
|
for (auto &i : hierarchy) {
|
|
auto viewHierarchyValue = i["name"];
|
|
if (!viewHierarchyValue.isNull()) {
|
|
result.hierarchy.emplace_back(viewHierarchyValue.c_str());
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
void Scheduler::unregisterSurface(
|
|
SurfaceHandler const &surfaceHandler) const noexcept {
|
|
surfaceHandler.setUIManager(nullptr);
|
|
}
|
|
|
|
void Scheduler::renderTemplateToSurface(
|
|
SurfaceId surfaceId,
|
|
const std::string &uiTemplate) {
|
|
SystraceSection s("Scheduler::renderTemplateToSurface");
|
|
try {
|
|
if (uiTemplate.empty()) {
|
|
return;
|
|
}
|
|
NativeModuleRegistry nMR;
|
|
auto tree = UITemplateProcessor::buildShadowTree(
|
|
uiTemplate,
|
|
surfaceId,
|
|
folly::dynamic::object(),
|
|
*componentDescriptorRegistry_,
|
|
nMR,
|
|
reactNativeConfig_);
|
|
|
|
uiManager_->getShadowTreeRegistry().visit(
|
|
surfaceId, [=](const ShadowTree &shadowTree) {
|
|
return shadowTree.tryCommit(
|
|
[&](RootShadowNode const &oldRootShadowNode) {
|
|
return std::make_shared<RootShadowNode>(
|
|
oldRootShadowNode,
|
|
ShadowNodeFragment{
|
|
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
|
|
/* .children = */
|
|
std::make_shared<ShadowNode::ListOfShared>(
|
|
ShadowNode::ListOfShared{tree}),
|
|
});
|
|
},
|
|
{/* default commit options */});
|
|
});
|
|
} catch (const std::exception &e) {
|
|
LOG(ERROR) << " >>>> EXCEPTION <<< rendering uiTemplate in "
|
|
<< "Scheduler::renderTemplateToSurface: " << e.what();
|
|
}
|
|
}
|
|
|
|
ComponentDescriptor const *
|
|
Scheduler::findComponentDescriptorByHandle_DO_NOT_USE_THIS_IS_BROKEN(
|
|
ComponentHandle handle) const {
|
|
return componentDescriptorRegistry_
|
|
->findComponentDescriptorByHandle_DO_NOT_USE_THIS_IS_BROKEN(handle);
|
|
}
|
|
|
|
#pragma mark - Delegate
|
|
|
|
void Scheduler::setDelegate(SchedulerDelegate *delegate) {
|
|
delegate_ = delegate;
|
|
}
|
|
|
|
SchedulerDelegate *Scheduler::getDelegate() const {
|
|
return delegate_;
|
|
}
|
|
|
|
#pragma mark - UIManagerAnimationDelegate
|
|
|
|
void Scheduler::animationTick() const {
|
|
uiManager_->animationTick();
|
|
}
|
|
|
|
#pragma mark - UIManagerDelegate
|
|
|
|
void Scheduler::uiManagerDidFinishTransaction(
|
|
MountingCoordinator::Shared mountingCoordinator,
|
|
bool mountSynchronously) {
|
|
SystraceSection s("Scheduler::uiManagerDidFinishTransaction");
|
|
|
|
if (delegate_ != nullptr) {
|
|
if (CoreFeatures::blockPaintForUseLayoutEffect) {
|
|
auto weakRuntimeScheduler =
|
|
contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
|
|
"RuntimeScheduler");
|
|
auto runtimeScheduler = weakRuntimeScheduler.has_value()
|
|
? weakRuntimeScheduler.value().lock()
|
|
: nullptr;
|
|
if (runtimeScheduler && !mountSynchronously) {
|
|
runtimeScheduler->scheduleTask(
|
|
SchedulerPriority::UserBlockingPriority,
|
|
[delegate = delegate_,
|
|
mountingCoordinator =
|
|
std::move(mountingCoordinator)](jsi::Runtime &) {
|
|
delegate->schedulerDidFinishTransaction(
|
|
std::move(mountingCoordinator));
|
|
});
|
|
} else {
|
|
delegate_->schedulerDidFinishTransaction(
|
|
std::move(mountingCoordinator));
|
|
}
|
|
} else {
|
|
delegate_->schedulerDidFinishTransaction(std::move(mountingCoordinator));
|
|
}
|
|
}
|
|
}
|
|
void Scheduler::uiManagerDidCreateShadowNode(const ShadowNode &shadowNode) {
|
|
SystraceSection s("Scheduler::uiManagerDidCreateShadowNode");
|
|
|
|
if (delegate_ != nullptr) {
|
|
delegate_->schedulerDidRequestPreliminaryViewAllocation(
|
|
shadowNode.getSurfaceId(), shadowNode);
|
|
}
|
|
}
|
|
|
|
void Scheduler::uiManagerDidDispatchCommand(
|
|
const ShadowNode::Shared &shadowNode,
|
|
std::string const &commandName,
|
|
folly::dynamic const &args) {
|
|
SystraceSection s("Scheduler::uiManagerDispatchCommand");
|
|
|
|
if (delegate_ != nullptr) {
|
|
auto shadowView = ShadowView(*shadowNode);
|
|
delegate_->schedulerDidDispatchCommand(shadowView, commandName, args);
|
|
}
|
|
}
|
|
|
|
void Scheduler::uiManagerDidSendAccessibilityEvent(
|
|
const ShadowNode::Shared &shadowNode,
|
|
std::string const &eventType) {
|
|
SystraceSection s("Scheduler::uiManagerDidSendAccessibilityEvent");
|
|
|
|
if (delegate_ != nullptr) {
|
|
auto shadowView = ShadowView(*shadowNode);
|
|
delegate_->schedulerDidSendAccessibilityEvent(shadowView, eventType);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set JS responder for a view.
|
|
*/
|
|
void Scheduler::uiManagerDidSetIsJSResponder(
|
|
ShadowNode::Shared const &shadowNode,
|
|
bool isJSResponder,
|
|
bool blockNativeResponder) {
|
|
if (delegate_ != nullptr) {
|
|
delegate_->schedulerDidSetIsJSResponder(
|
|
ShadowView(*shadowNode), isJSResponder, blockNativeResponder);
|
|
}
|
|
}
|
|
|
|
ContextContainer::Shared Scheduler::getContextContainer() const {
|
|
return contextContainer_;
|
|
}
|
|
|
|
std::shared_ptr<UIManager> Scheduler::getUIManager() const {
|
|
return uiManager_;
|
|
}
|
|
|
|
void Scheduler::addEventListener(
|
|
const std::shared_ptr<EventListener const> &listener) {
|
|
if (eventDispatcher_->has_value()) {
|
|
eventDispatcher_->value().addListener(listener);
|
|
}
|
|
}
|
|
|
|
void Scheduler::removeEventListener(
|
|
const std::shared_ptr<EventListener const> &listener) {
|
|
if (eventDispatcher_->has_value()) {
|
|
eventDispatcher_->value().removeListener(listener);
|
|
}
|
|
}
|
|
|
|
} // namespace facebook::react
|