first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_mounting_SRC CONFIGURE_DEPENDS
*.cpp
internal/*.cpp
stubs/*.cpp)
add_library(react_renderer_mounting OBJECT ${react_renderer_mounting_SRC})
target_include_directories(react_renderer_mounting PRIVATE .)
target_include_directories(react_renderer_mounting PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_mounting
folly_runtime
glog
glog_init
jsi
react_debug
react_renderer_core
react_renderer_debug
react_renderer_graphics
react_renderer_telemetry
react_utils
rrc_root
rrc_view
rrc_scrollview
yoga)
target_compile_reactnative_options(react_renderer_mounting PRIVATE)
target_compile_options(react_renderer_mounting PRIVATE -Wpedantic)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
namespace facebook::react {
/*
* Calculates a list of view mutations which describes how the old
* `ShadowTree` can be transformed to the new one.
* The list of mutations might be and might not be optimal.
*/
ShadowViewMutation::List calculateShadowViewMutations(
const ShadowNode &oldRootShadowNode,
const ShadowNode &newRootShadowNode);
} // namespace facebook::react

View File

@@ -0,0 +1,228 @@
/*
* 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 "MountingCoordinator.h"
#include <cxxreact/TraceSection.h>
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/utils/LowPriorityExecutor.h>
#include <condition_variable>
#include "updateMountedFlag.h"
#ifdef RN_SHADOW_TREE_INTROSPECTION
#include <glog/logging.h>
#include <sstream>
#endif
namespace facebook::react {
MountingCoordinator::MountingCoordinator(const ShadowTreeRevision& baseRevision)
: surfaceId_(baseRevision.rootShadowNode->getSurfaceId()),
baseRevision_(baseRevision),
telemetryController_(*this) {
#ifdef RN_SHADOW_TREE_INTROSPECTION
stubViewTree_ = buildStubViewTreeWithoutUsingDifferentiator(
*baseRevision_.rootShadowNode);
#endif
}
SurfaceId MountingCoordinator::getSurfaceId() const {
return surfaceId_;
}
void MountingCoordinator::push(ShadowTreeRevision revision) const {
{
std::scoped_lock lock(mutex_);
react_native_assert(
!lastRevision_.has_value() || revision.number != lastRevision_->number);
if (!lastRevision_.has_value() || lastRevision_->number < revision.number) {
lastRevision_ = std::move(revision);
}
}
signal_.notify_all();
}
void MountingCoordinator::revoke() const {
std::scoped_lock lock(mutex_);
// We have two goals here.
// 1. We need to stop retaining `ShadowNode`s to not prolong their lifetime
// to prevent them from overliving `ComponentDescriptor`s.
// 2. A possible call to `pullTransaction()` should return empty optional.
baseRevision_.rootShadowNode.reset();
lastRevision_.reset();
}
bool MountingCoordinator::waitForTransaction(
std::chrono::duration<double> timeout) const {
std::unique_lock<std::mutex> lock(mutex_);
return signal_.wait_for(
lock, timeout, [this]() { return lastRevision_.has_value(); });
}
void MountingCoordinator::updateBaseRevision(
const ShadowTreeRevision& baseRevision) const {
std::scoped_lock lock(mutex_);
baseRevision_ = baseRevision;
}
void MountingCoordinator::resetLatestRevision() const {
std::scoped_lock lock(mutex_);
lastRevision_.reset();
}
std::optional<MountingTransaction> MountingCoordinator::pullTransaction(
bool willPerformAsynchronously) const {
TraceSection section("MountingCoordinator::pullTransaction");
std::scoped_lock lock(mutex_);
auto transaction = std::optional<MountingTransaction>{};
// Base case
if (lastRevision_.has_value()) {
number_++;
auto telemetry = lastRevision_->telemetry;
telemetry.willDiff();
auto mutations = calculateShadowViewMutations(
*baseRevision_.rootShadowNode, *lastRevision_->rootShadowNode);
telemetry.didDiff();
transaction = MountingTransaction{
surfaceId_, number_, std::move(mutations), telemetry};
}
// Override case
#ifdef RN_SHADOW_TREE_INTROSPECTION
bool didOverridePullTransaction = false;
#endif
for (const auto& delegate : mountingOverrideDelegates_) {
auto mountingOverrideDelegate = delegate.lock();
auto shouldOverridePullTransaction = mountingOverrideDelegate &&
mountingOverrideDelegate->shouldOverridePullTransaction();
if (shouldOverridePullTransaction) {
TraceSection section2("MountingCoordinator::overridePullTransaction");
auto mutations = ShadowViewMutation::List{};
auto telemetry = TransactionTelemetry{};
if (transaction.has_value()) {
mutations = transaction->getMutations();
telemetry = transaction->getTelemetry();
} else {
number_++;
telemetry.willLayout();
telemetry.didLayout();
telemetry.willCommit();
telemetry.didCommit();
telemetry.willDiff();
telemetry.didDiff();
}
transaction = mountingOverrideDelegate->pullTransaction(
surfaceId_, number_, telemetry, std::move(mutations));
#ifdef RN_SHADOW_TREE_INTROSPECTION
didOverridePullTransaction = true;
#endif
}
}
#ifdef RN_SHADOW_TREE_INTROSPECTION
if (transaction.has_value()) {
TraceSection section2("MountingCoordinator::verifyMutationsForDebugging");
// We have something to validate.
auto mutations = transaction->getMutations();
// No matter what the source of the transaction is, it must be able to
// mutate the existing stub view tree.
stubViewTree_.mutate(mutations);
// If the transaction was overridden, we don't have a model of the shadow
// tree therefore we cannot validate the validity of the mutation
// instructions.
if (!didOverridePullTransaction && lastRevision_.has_value()) {
auto stubViewTree = buildStubViewTreeWithoutUsingDifferentiator(
*lastRevision_->rootShadowNode);
bool treesEqual = stubViewTree_ == stubViewTree;
if (!treesEqual) {
// Display debug info
auto line = std::string{};
std::stringstream ssOldTree(
baseRevision_.rootShadowNode->getDebugDescription());
while (std::getline(ssOldTree, line, '\n')) {
LOG(ERROR) << "Old tree:" << line;
}
std::stringstream ssMutations(getDebugDescription(mutations, {}));
while (std::getline(ssMutations, line, '\n')) {
LOG(ERROR) << "Mutations:" << line;
}
std::stringstream ssNewTree(
lastRevision_->rootShadowNode->getDebugDescription());
while (std::getline(ssNewTree, line, '\n')) {
LOG(ERROR) << "New tree:" << line;
}
}
react_native_assert(
(treesEqual) && "Incorrect set of mutations detected.");
}
}
#endif
if (lastRevision_.has_value()) {
if (ReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync()) {
LowPriorityExecutor::execute([toDelete = std::move(baseRevision_)]() {});
}
baseRevision_ = std::move(*lastRevision_);
lastRevision_.reset();
hasPendingTransactionsOverride_ = willPerformAsynchronously;
}
return transaction;
}
bool MountingCoordinator::hasPendingTransactions() const {
std::scoped_lock lock(mutex_);
return lastRevision_.has_value() || hasPendingTransactionsOverride_;
}
void MountingCoordinator::didPerformAsyncTransactions() const {
std::scoped_lock lock(mutex_);
hasPendingTransactionsOverride_ = false;
}
const TelemetryController& MountingCoordinator::getTelemetryController() const {
return telemetryController_;
}
ShadowTreeRevision MountingCoordinator::getBaseRevision() const {
std::scoped_lock lock(mutex_);
return baseRevision_;
}
void MountingCoordinator::setMountingOverrideDelegate(
std::weak_ptr<const MountingOverrideDelegate> delegate) const {
std::scoped_lock lock(mutex_);
mountingOverrideDelegates_.insert(
mountingOverrideDelegates_.end(), std::move(delegate));
}
} // namespace facebook::react

View File

@@ -0,0 +1,146 @@
/*
* 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.
*/
#pragma once
#include <chrono>
#include <condition_variable>
#include <optional>
#include <react/renderer/debug/flags.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/MountingOverrideDelegate.h>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/renderer/mounting/TelemetryController.h>
#ifdef RN_SHADOW_TREE_INTROSPECTION
#include <react/renderer/mounting/stubs/stubs.h>
#endif
namespace facebook::react {
/*
* Stores inside all non-mounted yet revisions of a shadow tree and coordinates
* mounting. The object stores the most recent mounted revision and the most
* recent committed one. Then when a new mounting transaction is requested the
* object generates mutation instructions and returns it as a
* `MountingTransaction`.
*/
class MountingCoordinator final {
public:
/*
* The constructor is meant to be used only inside `ShadowTree`, and it's
* `public` only to enable using with `std::make_shared<>`.
*/
MountingCoordinator(const ShadowTreeRevision &baseRevision);
/*
* Returns the id of the surface that the coordinator belongs to.
*/
SurfaceId getSurfaceId() const;
/*
* Computes a consequent mounting transaction and returns it.
* The returning transaction can accumulate multiple recent revisions of a
* shadow tree. Returns empty optional if there no new shadow tree revision to
* mount.
* The method is thread-safe and can be called from any thread.
* However, a consumer should always call it on the same thread (e.g. on the
* main thread) or ensure sequentiality of mount transactions separately.
*
* `willPerformAsynchronously` indicates if this transaction is going to be
* applied asynchronously after this call. The preferred model is to apply
* them synchronously but Android doesn't follow it yet (it calls
* `pullTransaction` from the JS thread and schedules the updates
* asynchronously on the UI thread).
* If this is `true`, then `hasPendingTransactions` will continue returning
* `true` until `didPerformAsyncTransactions` is called.
*/
std::optional<MountingTransaction> pullTransaction(bool willPerformAsynchronously = false) const;
/*
* This method is used to notify that transactions that weren't performed
* synchronously when calling `pullTransaction` were effectively applied
* on the UI thread.
*
* NOTE: This is only necessary on Android and can be removed when it's
* migrated to a pull model (as the rest of platforms).
*/
void didPerformAsyncTransactions() const;
/*
* Indicates if there are transactions waiting to be consumed and mounted on
* the host platform. This can be useful to determine if side-effects of
* mounting can be expected after some operations (like IntersectionObserver
* initial paint notifications).
*/
bool hasPendingTransactions() const;
/*
* Blocks the current thread until a new mounting transaction is available or
* after the specified `timeout` duration.
* Returns `false` if a timeout occurred before a new transaction available.
* Call `pullTransaction` right after the method to retrieve the transaction.
* Similarly to `pullTransaction` this method is thread-safe but the consumer
* should call it on the same thread (e.g. on the main thread) or ensure
* sequentiality of mount transactions separately.
*/
bool waitForTransaction(std::chrono::duration<double> timeout) const;
const TelemetryController &getTelemetryController() const;
ShadowTreeRevision getBaseRevision() const;
/*
* Methods from this section are meant to be used by
* `MountingOverrideDelegate` only.
*/
public:
void updateBaseRevision(const ShadowTreeRevision &baseRevision) const;
void resetLatestRevision() const;
void setMountingOverrideDelegate(std::weak_ptr<const MountingOverrideDelegate> delegate) const;
/*
* Methods from this section are meant to be used by `ShadowTree` only.
*/
private:
friend class ShadowTree;
void push(ShadowTreeRevision revision) const;
/*
* Revokes the last pushed `ShadowTreeRevision`.
* Generating a `MountingTransaction` requires some resources which the
* `MountingCoordinator` does not own (e.g. `ComponentDescriptor`s). Revoking
* committed revisions allows the owner (a Shadow Tree) to make sure that
* those resources will not be accessed (e.g. by the Mounting Layer).
*/
void revoke() const;
private:
const SurfaceId surfaceId_;
// Protects access to `baseRevision_`, `lastRevision_` and
// `mountingOverrideDelegate_`.
mutable std::mutex mutex_;
mutable ShadowTreeRevision baseRevision_;
mutable bool hasPendingTransactionsOverride_{false};
mutable std::optional<ShadowTreeRevision> lastRevision_{};
mutable MountingTransaction::Number number_{0};
mutable std::condition_variable signal_;
mutable std::vector<std::weak_ptr<const MountingOverrideDelegate>> mountingOverrideDelegates_;
TelemetryController telemetryController_;
#ifdef RN_SHADOW_TREE_INTROSPECTION
mutable StubViewTree stubViewTree_; // Protected by `mutex_`.
#endif
};
} // namespace facebook::react

View File

@@ -0,0 +1,46 @@
/*
* 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 <react/renderer/mounting/MountingTransaction.h>
#pragma once
namespace facebook::react {
class MountingCoordinator;
/**
* Generic interface for anything that needs to override specific
* MountingCoordinator methods. This is for platform-specific escape hatches
* like animations.
*/
class MountingOverrideDelegate {
public:
virtual bool shouldOverridePullTransaction() const = 0;
virtual ~MountingOverrideDelegate() = default;
/**
* Delegates that override this method are responsible for:
*
* - Returning a MountingTransaction with mutations
* - Calling
* - Telemetry, if appropriate
*
* @param surfaceId the Id of the surface to be mounted
* @param number of the transaction
* @param telemetry object associated with the transaction
* @param mutations list of the mutations from the ShadowTree
* @return the mounting transaction, if it was created
*/
virtual std::optional<MountingTransaction> pullTransaction(
SurfaceId surfaceId,
MountingTransaction::Number number,
const TransactionTelemetry &telemetry,
ShadowViewMutationList mutations) const = 0;
};
} // namespace facebook::react

View File

@@ -0,0 +1,57 @@
/*
* 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 "MountingTransaction.h"
namespace facebook::react {
using Number = MountingTransaction::Number;
MountingTransaction::MountingTransaction(
SurfaceId surfaceId,
Number number,
ShadowViewMutationList&& mutations,
TransactionTelemetry telemetry)
: surfaceId_(surfaceId),
number_(number),
mutations_(std::move(mutations)),
telemetry_(std::move(telemetry)) {}
const ShadowViewMutationList& MountingTransaction::getMutations() const& {
return mutations_;
}
ShadowViewMutationList MountingTransaction::getMutations() && {
return std::move(mutations_);
}
TransactionTelemetry& MountingTransaction::getTelemetry() const {
return telemetry_;
}
SurfaceId MountingTransaction::getSurfaceId() const {
return surfaceId_;
}
Number MountingTransaction::getNumber() const {
return number_;
}
void MountingTransaction::mergeWith(MountingTransaction&& transaction) {
react_native_assert(transaction.getSurfaceId() == surfaceId_);
number_ = transaction.getNumber();
mutations_.insert(
mutations_.end(),
std::make_move_iterator(transaction.mutations_.begin()),
std::make_move_iterator(transaction.mutations_.end()));
// TODO T186641819: Telemetry for merged transactions is not supported, use
// the latest instance
telemetry_ = std::move(transaction.telemetry_);
}
} // namespace facebook::react

View File

@@ -0,0 +1,93 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/SurfaceTelemetry.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
/*
* Encapsulates all artifacts of `ShadowTree` commit (or a series of them),
* particularly list of mutations and meta-data associated with the commit.
* Movable and copyable, but moving is strongly encouraged.
* Beware: A moved-from object of this type has unspecified value and accessing
* that is UB (Undefined Behaviour).
*/
class MountingTransaction final {
public:
/*
* A Number (or revision) grows continuously starting from `1`. Value `0`
* represents the state before the very first transaction happens.
*/
using Number = int64_t;
/*
* Copying a list of `ShadowViewMutation` is expensive, so the constructor
* accepts it as rvalue reference to discourage copying.
*/
MountingTransaction(
SurfaceId surfaceId,
Number number,
ShadowViewMutationList &&mutations,
TransactionTelemetry telemetry);
/*
* Copy semantic.
* Copying of MountingTransaction is expensive, so copy-constructor is
* explicit and copy-assignment is deleted to prevent accidental copying.
*/
explicit MountingTransaction(const MountingTransaction &mountingTransaction) = default;
MountingTransaction &operator=(const MountingTransaction &other) = delete;
/*
* Move semantic.
*/
MountingTransaction(MountingTransaction &&mountingTransaction) noexcept = default;
MountingTransaction &operator=(MountingTransaction &&other) = default;
/*
* Returns a list of mutations that represent the transaction. The list can be
* empty (theoretically).
*/
const ShadowViewMutationList &getMutations() const &;
ShadowViewMutationList getMutations() &&;
/*
* Returns telemetry associated with this transaction.
*/
TransactionTelemetry &getTelemetry() const;
/*
* Returns the id of the surface that the transaction belongs to.
*/
SurfaceId getSurfaceId() const;
/*
* Returns a sequential number of the particular transaction.
*/
Number getNumber() const;
/*
* Merges the given transaction in the current transaction, so they
* can be executed atomatically as a single transaction.
*
* This is required for Android UI, which needs to separately apply
* each ShadowTree mutation due to differences in props representation.
*/
void mergeWith(MountingTransaction &&transaction);
private:
SurfaceId surfaceId_;
Number number_;
ShadowViewMutationList mutations_;
mutable TransactionTelemetry telemetry_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,445 @@
/*
* 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 "ShadowTree.h"
#include <cxxreact/TraceSection.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/core/LayoutPrimitives.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
#include "updateMountedFlag.h"
#include "ShadowTreeDelegate.h"
namespace facebook::react {
namespace {
const int MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING = 3;
} // namespace
using CommitStatus = ShadowTree::CommitStatus;
using CommitMode = ShadowTree::CommitMode;
/*
* Generates (possibly) a new tree where all nodes with non-obsolete `State`
* objects. If all `State` objects in the tree are not obsolete for the moment
* of calling, the function returns `nullptr` (as an indication that no
* additional work is required).
*/
static std::shared_ptr<ShadowNode> progressState(const ShadowNode& shadowNode) {
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto newChildren = std::vector<std::shared_ptr<const ShadowNode>>{};
if (!shadowNode.getChildren().empty()) {
auto index = size_t{0};
for (const auto& childNode : shadowNode.getChildren()) {
auto newChildNode = progressState(*childNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = shadowNode.getChildren();
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
index++;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
.props = ShadowNodeFragment::propsPlaceholder(),
.children = areChildrenChanged
? std::make_shared<
const std::vector<std::shared_ptr<const ShadowNode>>>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
.state =
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
/*
* An optimized version of the previous function (and relies on it).
* The function uses a given base tree to exclude unchanged (equal) parts
* of the three from the traversing.
*/
static std::shared_ptr<ShadowNode> progressState(
const ShadowNode& shadowNode,
const ShadowNode& baseShadowNode) {
// The intuition behind the complexity:
// - A very few nodes have associated state, therefore it's mostly reading and
// it only writes when state objects were found obsolete;
// - Most before-after trees are aligned, therefore most tree branches will be
// skipped;
// - If trees are significantly different, any other algorithm will have
// close to linear complexity.
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto& children = shadowNode.getChildren();
auto& baseChildren = baseShadowNode.getChildren();
auto newChildren = std::vector<std::shared_ptr<const ShadowNode>>{};
auto childrenSize = children.size();
auto baseChildrenSize = baseChildren.size();
auto index = size_t{0};
// Stage 1: Aligned part.
for (index = 0; index < childrenSize && index < baseChildrenSize; index++) {
const auto& childNode = *children[index];
const auto& baseChildNode = *baseChildren[index];
if (&childNode == &baseChildNode) {
// Nodes are identical, skipping.
continue;
}
if (!ShadowNode::sameFamily(childNode, baseChildNode)) {
// Totally different nodes, updating is impossible.
break;
}
auto newChildNode = progressState(childNode, baseChildNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
// Stage 2: Misaligned part.
for (; index < childrenSize; index++) {
auto newChildNode = progressState(*children[index]);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
.props = ShadowNodeFragment::propsPlaceholder(),
.children = areChildrenChanged
? std::make_shared<
const std::vector<std::shared_ptr<const ShadowNode>>>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
.state =
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
ShadowTree::ShadowTree(
SurfaceId surfaceId,
const LayoutConstraints& layoutConstraints,
const LayoutContext& layoutContext,
const ShadowTreeDelegate& delegate,
const ContextContainer& contextContainer)
: surfaceId_(surfaceId), delegate_(delegate) {
static RootComponentDescriptor globalRootComponentDescriptor(
ComponentDescriptorParameters{
.eventDispatcher = EventDispatcher::Shared{},
.contextContainer = nullptr,
.flavor = nullptr});
const auto props = std::make_shared<const RootProps>(
PropsParserContext{surfaceId, contextContainer},
*RootShadowNode::defaultSharedProps(),
layoutConstraints,
layoutContext);
auto family = globalRootComponentDescriptor.createFamily(
{.tag = surfaceId, .surfaceId = surfaceId, .instanceHandle = nullptr});
auto rootShadowNode = std::static_pointer_cast<const RootShadowNode>(
globalRootComponentDescriptor.createShadowNode(
ShadowNodeFragment{
/* .props = */ .props = props,
},
family));
currentRevision_ = ShadowTreeRevision{
.rootShadowNode = rootShadowNode,
.number = INITIAL_REVISION,
.telemetry = TransactionTelemetry{}};
mountingCoordinator_ =
std::make_shared<const MountingCoordinator>(currentRevision_);
}
ShadowTree::~ShadowTree() {
mountingCoordinator_->revoke();
}
Tag ShadowTree::getSurfaceId() const {
return surfaceId_;
}
void ShadowTree::setCommitMode(CommitMode commitMode) const {
auto revision = ShadowTreeRevision{};
{
ShadowTree::UniqueLock lock = uniqueCommitLock();
if (commitMode_ == commitMode) {
return;
}
commitMode_ = commitMode;
revision = currentRevision_;
}
// initial revision never contains any commits so mounting it here is
// incorrect
if (commitMode == CommitMode::Normal && revision.number != INITIAL_REVISION) {
mount(revision, true);
}
}
CommitMode ShadowTree::getCommitMode() const {
SharedLock lock = sharedCommitLock();
return commitMode_;
}
std::shared_ptr<const MountingCoordinator> ShadowTree::getMountingCoordinator()
const {
return mountingCoordinator_;
}
CommitStatus ShadowTree::commit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
[[maybe_unused]] int attempts = 0;
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
while (attempts < MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING) {
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
}
attempts++;
}
{
std::unique_lock lock(commitMutexRecursive_);
return tryCommit(transaction, commitOptions);
}
} else {
while (true) {
attempts++;
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
}
// After multiple attempts, we failed to commit the transaction.
// Something internally went terribly wrong.
react_native_assert(attempts < 1024);
}
}
}
CommitStatus ShadowTree::tryCommit(
const ShadowTreeCommitTransaction& transaction,
const CommitOptions& commitOptions) const {
TraceSection s("ShadowTree::commit");
auto telemetry = TransactionTelemetry{};
telemetry.willCommit();
CommitMode commitMode;
auto oldRevision = ShadowTreeRevision{};
auto newRevision = ShadowTreeRevision{};
{
// Reading `currentRevision_` in shared manner.
SharedLock lock = sharedCommitLock();
commitMode = commitMode_;
oldRevision = currentRevision_;
}
const auto& oldRootShadowNode = oldRevision.rootShadowNode;
auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);
if (!newRootShadowNode) {
return CommitStatus::Cancelled;
}
if (commitOptions.enableStateReconciliation) {
auto updatedNewRootShadowNode =
progressState(*newRootShadowNode, *oldRootShadowNode);
if (updatedNewRootShadowNode) {
newRootShadowNode =
std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
}
}
// Run commit hooks.
newRootShadowNode = delegate_.shadowTreeWillCommit(
*this, oldRootShadowNode, newRootShadowNode, commitOptions);
if (!newRootShadowNode) {
return CommitStatus::Cancelled;
}
// Layout nodes.
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
telemetry.willLayout();
telemetry.setAsThreadLocal();
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
telemetry.unsetAsThreadLocal();
telemetry.didLayout(static_cast<int>(affectedLayoutableNodes.size()));
{
// Updating `currentRevision_` in unique manner if it hasn't changed.
UniqueLock lock = uniqueCommitLock();
if (currentRevision_.number != oldRevision.number) {
return CommitStatus::Failed;
}
auto newRevisionNumber = currentRevision_.number + 1;
{
std::scoped_lock dispatchLock(EventEmitter::DispatchMutex());
updateMountedFlag(
currentRevision_.rootShadowNode->getChildren(),
newRootShadowNode->getChildren(),
commitOptions.source);
}
telemetry.didCommit();
telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));
// Seal the shadow node so it can no longer be mutated
// Does nothing in release.
newRootShadowNode->sealRecursive();
newRevision = ShadowTreeRevision{
.rootShadowNode = std::move(newRootShadowNode),
.number = newRevisionNumber,
.telemetry = telemetry};
currentRevision_ = newRevision;
}
emitLayoutEvents(affectedLayoutableNodes);
if (commitMode == CommitMode::Normal) {
mount(std::move(newRevision), commitOptions.mountSynchronously);
}
return CommitStatus::Succeeded;
}
ShadowTreeRevision ShadowTree::getCurrentRevision() const {
SharedLock lock = sharedCommitLock();
return currentRevision_;
}
void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously)
const {
mountingCoordinator_->push(std::move(revision));
delegate_.shadowTreeDidFinishTransaction(
mountingCoordinator_, mountSynchronously);
}
void ShadowTree::commitEmptyTree() const {
commit(
[](const RootShadowNode& oldRootShadowNode)
-> std::shared_ptr<RootShadowNode> {
return std::make_shared<RootShadowNode>(
oldRootShadowNode,
ShadowNodeFragment{
/* .props = */ .props = ShadowNodeFragment::propsPlaceholder(),
/* .children = */ .children =
ShadowNode::emptySharedShadowNodeSharedList(),
});
},
{/* default commit options */});
}
void ShadowTree::emitLayoutEvents(
std::vector<const LayoutableShadowNode*>& affectedLayoutableNodes) const {
TraceSection s(
"ShadowTree::emitLayoutEvents",
"affectedLayoutableNodes",
affectedLayoutableNodes.size());
for (const auto* layoutableNode : affectedLayoutableNodes) {
if (auto viewProps =
dynamic_cast<const ViewProps*>(layoutableNode->getProps().get())) {
if (viewProps->onLayout) {
static_cast<const BaseViewEventEmitter&>(
*layoutableNode->getEventEmitter())
.onLayout(layoutableNode->getLayoutMetrics());
}
}
}
}
void ShadowTree::notifyDelegatesOfUpdates() const {
delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true);
}
inline ShadowTree::UniqueLock ShadowTree::uniqueCommitLock() const {
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
return std::unique_lock{commitMutexRecursive_};
} else {
return std::unique_lock{commitMutex_};
}
}
inline ShadowTree::SharedLock ShadowTree::sharedCommitLock() const {
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
return std::unique_lock{commitMutexRecursive_};
} else {
return std::shared_lock{commitMutex_};
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,161 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/MountingCoordinator.h>
#include <react/renderer/mounting/ShadowTreeDelegate.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/utils/ContextContainer.h>
#include "MountingOverrideDelegate.h"
namespace facebook::react {
using ShadowTreeCommitTransaction = std::function<RootShadowNode::Unshared(const RootShadowNode &oldRootShadowNode)>;
/*
* Represents a result of a `commit` operation.
*/
enum class ShadowTreeCommitStatus {
Succeeded,
Failed,
Cancelled,
};
/*
* Represents commits' side-effects propagation mode.
*/
enum class ShadowTreeCommitMode {
// Commits' side-effects are observable via `MountingCoordinator`.
// The rendering pipeline fully works end-to-end.
Normal,
// Commits' side-effects are *not* observable via `MountingCoordinator`.
// The mounting phase is skipped in the rendering pipeline.
Suspended,
};
enum class ShadowTreeCommitSource {
Unknown,
React,
};
struct ShadowTreeCommitOptions {
// When set to true, Shadow Node state from current revision will be applied
// to the new revision. For more details see
// https://reactnative.dev/architecture/render-pipeline#react-native-renderer-state-updates
bool enableStateReconciliation{false};
// Indicates if mounting will be triggered synchronously and React will
// not get a chance to interrupt painting.
// This should be set to `false` when a commit is coming from React. It
// will then let React run layout effects and apply updates before paint.
// For all other commits, should be true.
bool mountSynchronously{true};
ShadowTreeCommitSource source{ShadowTreeCommitSource::Unknown};
};
/*
* Represents the shadow tree and its lifecycle.
*/
class ShadowTree final {
public:
using Unique = std::unique_ptr<ShadowTree>;
using CommitStatus = ShadowTreeCommitStatus;
using CommitMode = ShadowTreeCommitMode;
using CommitSource = ShadowTreeCommitSource;
using CommitOptions = ShadowTreeCommitOptions;
/*
* Creates a new shadow tree instance.
*/
ShadowTree(
SurfaceId surfaceId,
const LayoutConstraints &layoutConstraints,
const LayoutContext &layoutContext,
const ShadowTreeDelegate &delegate,
const ContextContainer &contextContainer);
~ShadowTree();
/*
* Returns the `SurfaceId` associated with the shadow tree.
*/
SurfaceId getSurfaceId() const;
/*
* Sets and gets the commit mode.
* Changing commit mode from `Suspended` to `Normal` will flush all suspended
* changes to `MountingCoordinator`.
*/
void setCommitMode(CommitMode commitMode) const;
CommitMode getCommitMode() const;
/*
* Performs commit calling `transaction` function with a `oldRootShadowNode`
* and expecting a `newRootShadowNode` as a return value.
* The `transaction` function can cancel commit returning `nullptr`.
*/
CommitStatus tryCommit(const ShadowTreeCommitTransaction &transaction, const CommitOptions &commitOptions) const;
/*
* Calls `tryCommit` in a loop until it finishes successfully.
*/
CommitStatus commit(const ShadowTreeCommitTransaction &transaction, const CommitOptions &commitOptions) const;
/*
* Returns a `ShadowTreeRevision` representing the momentary state of
* the `ShadowTree`.
*/
ShadowTreeRevision getCurrentRevision() const;
/*
* Commit an empty tree (a new `RootShadowNode` with no children).
*/
void commitEmptyTree() const;
/**
* Forces the ShadowTree to ping its delegate that an update is available.
* Useful for animations on Android.
*/
void notifyDelegatesOfUpdates() const;
std::shared_ptr<const MountingCoordinator> getMountingCoordinator() const;
private:
constexpr static ShadowTreeRevision::Number INITIAL_REVISION{0};
void mount(ShadowTreeRevision revision, bool mountSynchronously) const;
void emitLayoutEvents(std::vector<const LayoutableShadowNode *> &affectedLayoutableNodes) const;
const SurfaceId surfaceId_;
const ShadowTreeDelegate &delegate_;
mutable std::shared_mutex commitMutex_;
mutable std::recursive_mutex commitMutexRecursive_;
mutable CommitMode commitMode_{CommitMode::Normal}; // Protected by `commitMutex_`.
mutable ShadowTreeRevision currentRevision_; // Protected by `commitMutex_`.
std::shared_ptr<const MountingCoordinator> mountingCoordinator_;
using UniqueLock = std::variant<std::unique_lock<std::shared_mutex>, std::unique_lock<std::recursive_mutex>>;
using SharedLock = std::variant<std::shared_lock<std::shared_mutex>, std::unique_lock<std::recursive_mutex>>;
inline UniqueLock uniqueCommitLock() const;
inline SharedLock sharedCommitLock() const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/mounting/MountingCoordinator.h>
namespace facebook::react {
class ShadowTree;
struct ShadowTreeCommitOptions;
/*
* Abstract class for ShadowTree's delegate.
*/
class ShadowTreeDelegate {
public:
/*
* Called right before a ShadowTree commits a new tree.
* The receiver can alter a new (proposed) shadow tree with another tree
* by returning the altered tree.
* Returning a `nullptr` cancels the commit.
*/
virtual RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree &shadowTree,
const RootShadowNode::Shared &oldRootShadowNode,
const RootShadowNode::Unshared &newRootShadowNode,
const ShadowTreeCommitOptions &commitOptions) const = 0;
/*
* Called right after Shadow Tree commit a new state of the tree.
*/
virtual void shadowTreeDidFinishTransaction(
std::shared_ptr<const MountingCoordinator> mountingCoordinator,
bool mountSynchronously) const = 0;
virtual ~ShadowTreeDelegate() noexcept = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,67 @@
/*
* 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 "ShadowTreeRegistry.h"
#include <react/debug/react_native_assert.h>
namespace facebook::react {
ShadowTreeRegistry::~ShadowTreeRegistry() {
react_native_assert(
registry_.empty() && "Deallocation of non-empty `ShadowTreeRegistry`.");
}
void ShadowTreeRegistry::add(std::unique_ptr<ShadowTree>&& shadowTree) const {
std::unique_lock lock(mutex_);
registry_.emplace(shadowTree->getSurfaceId(), std::move(shadowTree));
}
std::unique_ptr<ShadowTree> ShadowTreeRegistry::remove(
SurfaceId surfaceId) const {
std::unique_lock lock(mutex_);
auto iterator = registry_.find(surfaceId);
if (iterator == registry_.end()) {
return {};
}
auto shadowTree = std::unique_ptr<ShadowTree>(iterator->second.release());
registry_.erase(iterator);
return shadowTree;
}
bool ShadowTreeRegistry::visit(
SurfaceId surfaceId,
const std::function<void(const ShadowTree& shadowTree)>& callback) const {
std::shared_lock lock(mutex_);
auto iterator = registry_.find(surfaceId);
if (iterator == registry_.end()) {
return false;
}
callback(*iterator->second);
return true;
}
void ShadowTreeRegistry::enumerate(
const std::function<void(const ShadowTree& shadowTree, bool& stop)>&
callback) const {
std::shared_lock lock(mutex_);
auto stop = false;
for (const auto& pair : registry_) {
callback(*pair.second, stop);
if (stop) {
return;
}
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,64 @@
/*
* 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.
*/
#pragma once
#include <shared_mutex>
#include <unordered_map>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/mounting/ShadowTree.h>
namespace facebook::react {
/*
* Owning registry of `ShadowTree`s.
*/
class ShadowTreeRegistry final {
public:
ShadowTreeRegistry() = default;
~ShadowTreeRegistry();
/*
* Adds a `ShadowTree` instance to the registry.
* The ownership of the instance is also transferred to the registry.
* Can be called from any thread.
*/
void add(std::unique_ptr<ShadowTree> &&shadowTree) const;
/*
* Removes a `ShadowTree` instance with given `surfaceId` from the registry
* and returns it as a result.
* The ownership of the instance is also transferred to the caller.
* Returns `nullptr` if a `ShadowTree` with given `surfaceId` was not found.
* Can be called from any thread.
*/
std::unique_ptr<ShadowTree> remove(SurfaceId surfaceId) const;
/*
* Finds a `ShadowTree` instance with a given `surfaceId` in the registry and
* synchronously calls the `callback` with a reference to the instance while
* the mutex is being acquired.
* Returns `true` if the registry has `ShadowTree` instance with corresponding
* `surfaceId`, otherwise returns `false` without calling the `callback`.
* Can be called from any thread.
*/
bool visit(SurfaceId surfaceId, const std::function<void(const ShadowTree &shadowTree)> &callback) const;
/*
* Enumerates all stored shadow trees.
* Set `stop` to `true` to interrupt the enumeration.
* Can be called from any thread.
*/
void enumerate(const std::function<void(const ShadowTree &shadowTree, bool &stop)> &callback) const;
private:
mutable std::shared_mutex mutex_;
mutable std::unordered_map<SurfaceId, std::unique_ptr<ShadowTree>> registry_; // Protected by `mutex_`.
};
} // namespace facebook::react

View File

@@ -0,0 +1,8 @@
/*
* 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 "ShadowTreeRevision.h"

View File

@@ -0,0 +1,38 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/mounting/MountingOverrideDelegate.h>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
/*
* Represent a particular committed state of a shadow tree. The object contains
* a pointer to a root shadow node, a sequential number of commit and telemetry.
*/
class ShadowTreeRevision final {
public:
/*
* Sequential number of the commit that created this revision of a shadow
* tree.
*/
using Number = int64_t;
friend class ShadowTree;
friend class MountingCoordinator;
RootShadowNode::Shared rootShadowNode;
Number number;
TransactionTelemetry telemetry;
};
} // namespace facebook::react

View File

@@ -0,0 +1,83 @@
/*
* 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 "ShadowView.h"
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/LayoutableShadowNode.h>
namespace facebook::react {
static LayoutMetrics layoutMetricsFromShadowNode(const ShadowNode& shadowNode) {
auto layoutableShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&shadowNode);
return layoutableShadowNode != nullptr
? layoutableShadowNode->getLayoutMetrics()
: EmptyLayoutMetrics;
}
ShadowView::ShadowView(const ShadowNode& shadowNode)
: componentName(shadowNode.getComponentName()),
componentHandle(shadowNode.getComponentHandle()),
surfaceId(shadowNode.getSurfaceId()),
tag(shadowNode.getTag()),
traits(shadowNode.getTraits()),
props(shadowNode.getProps()),
eventEmitter(shadowNode.getEventEmitter()),
layoutMetrics(layoutMetricsFromShadowNode(shadowNode)),
state(shadowNode.getState()) {}
bool ShadowView::operator==(const ShadowView& rhs) const {
return std::tie(
this->surfaceId,
this->tag,
this->componentName,
this->props,
this->eventEmitter,
this->layoutMetrics,
this->state) ==
std::tie(
rhs.surfaceId,
rhs.tag,
rhs.componentName,
rhs.props,
rhs.eventEmitter,
rhs.layoutMetrics,
rhs.state);
}
bool ShadowView::operator!=(const ShadowView& rhs) const {
return !(*this == rhs);
}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowView& object) {
return object.componentHandle == 0 ? "Invalid" : object.componentName;
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowView& object,
DebugStringConvertibleOptions options) {
return {
{.name = "surfaceId",
.value = getDebugDescription(object.surfaceId, options)},
{.name = "tag", .value = getDebugDescription(object.tag, options)},
{.name = "traits", .value = getDebugDescription(object.traits, options)},
{.name = "componentName", .value = object.componentName},
{.name = "props", .value = getDebugDescription(object.props, options)},
{.name = "eventEmitter",
.value = getDebugDescription(object.eventEmitter, options)},
{.name = "layoutMetrics",
.value = getDebugDescription(object.layoutMetrics, options)},
{.name = "state", .value = getDebugDescription(object.state, options)},
};
}
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,80 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/EventEmitter.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/Props.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/debug/flags.h>
#include <react/utils/hash_combine.h>
namespace facebook::react {
/*
* Describes a view that can be mounted.
* This is exposed to the mounting layer.
*/
struct ShadowView final {
ShadowView() = default;
ShadowView(const ShadowView &shadowView) = default;
ShadowView(ShadowView &&shadowView) noexcept = default;
/*
* Constructs a `ShadowView` from given `ShadowNode`.
*/
explicit ShadowView(const ShadowNode &shadowNode);
ShadowView &operator=(const ShadowView &other) = default;
ShadowView &operator=(ShadowView &&other) = default;
bool operator==(const ShadowView &rhs) const;
bool operator!=(const ShadowView &rhs) const;
ComponentName componentName{};
ComponentHandle componentHandle{};
SurfaceId surfaceId{};
Tag tag{};
ShadowNodeTraits traits{};
Props::Shared props{};
EventEmitter::Shared eventEmitter{};
LayoutMetrics layoutMetrics{EmptyLayoutMetrics};
State::Shared state{};
};
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowView &object);
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowView &object,
DebugStringConvertibleOptions options);
#endif
} // namespace facebook::react
namespace std {
template <>
struct hash<facebook::react::ShadowView> {
size_t operator()(const facebook::react::ShadowView &shadowView) const
{
return facebook::react::hash_combine(
0,
shadowView.surfaceId,
shadowView.componentHandle,
shadowView.tag,
shadowView.props,
shadowView.eventEmitter,
shadowView.layoutMetrics,
shadowView.state);
}
};
} // namespace std

View File

@@ -0,0 +1,150 @@
/*
* 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 "ShadowViewMutation.h"
#include <utility>
namespace facebook::react {
ShadowViewMutation ShadowViewMutation::CreateMutation(ShadowView shadowView) {
return {
/* .type = */ Create,
/* .parentTag = */ -1,
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(shadowView),
/* .index = */ -1,
};
}
ShadowViewMutation ShadowViewMutation::DeleteMutation(ShadowView shadowView) {
return {
/* .type = */ Delete,
/* .parentTag = */ -1,
/* .oldChildShadowView = */ std::move(shadowView),
/* .newChildShadowView = */ {},
/* .index = */ -1,
};
}
ShadowViewMutation ShadowViewMutation::InsertMutation(
Tag parentTag,
ShadowView childShadowView,
int index) {
return {
/* .type = */ Insert,
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ {},
/* .newChildShadowView = */ std::move(childShadowView),
/* .index = */ index,
};
}
ShadowViewMutation ShadowViewMutation::RemoveMutation(
Tag parentTag,
ShadowView childShadowView,
int index) {
return {
/* .type = */ Remove,
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ std::move(childShadowView),
/* .newChildShadowView = */ {},
/* .index = */ index,
};
}
ShadowViewMutation ShadowViewMutation::UpdateMutation(
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
Tag parentTag) {
return {
/* .type = */ Update,
/* .parentTag = */ parentTag,
/* .oldChildShadowView = */ std::move(oldChildShadowView),
/* .newChildShadowView = */ std::move(newChildShadowView),
/* .index = */ -1,
};
}
bool ShadowViewMutation::mutatedViewIsVirtual() const {
bool viewIsVirtual = false;
#ifdef ANDROID
// Explanation: Even for non-virtual views,
// for "Insert" mutations, oldChildShadowView is always empty.
// for "Remove" mutations, newChildShadowView is always empty.
// Thus, to see if a view is virtual, we need to always check both the old and
// new View.
viewIsVirtual = newChildShadowView.layoutMetrics == EmptyLayoutMetrics &&
oldChildShadowView.layoutMetrics == EmptyLayoutMetrics;
#endif
return viewIsVirtual;
}
ShadowViewMutation::ShadowViewMutation(
Type type,
Tag parentTag,
ShadowView oldChildShadowView,
ShadowView newChildShadowView,
int index)
: type(type),
parentTag(parentTag),
oldChildShadowView(std::move(oldChildShadowView)),
newChildShadowView(std::move(newChildShadowView)),
index(index) {}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowViewMutation& mutation) {
switch (mutation.type) {
case ShadowViewMutation::Create:
return "Create";
case ShadowViewMutation::Delete:
return "Delete";
case ShadowViewMutation::Insert:
return "Insert";
case ShadowViewMutation::Remove:
return "Remove";
case ShadowViewMutation::Update:
return "Update";
}
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowViewMutation& mutation,
DebugStringConvertibleOptions options) {
return {
mutation.oldChildShadowView.componentHandle != 0
? DebugStringConvertibleObject{"oldChild",
getDebugDescription(
mutation.oldChildShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.newChildShadowView.componentHandle != 0
? DebugStringConvertibleObject{"newChild",
getDebugDescription(
mutation.newChildShadowView,
options)}
: DebugStringConvertibleObject{},
mutation.parentTag != -1
? DebugStringConvertibleObject{"parent",
getDebugDescription(
mutation.parentTag,
options)}
: DebugStringConvertibleObject{},
mutation.index != -1
? DebugStringConvertibleObject{"index",
getDebugDescription(
mutation.index, options)}
: DebugStringConvertibleObject{},
};
}
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,122 @@
/*
* 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.
*/
#pragma once
#include <vector>
#include <react/renderer/mounting/ShadowView.h>
namespace facebook::react {
/**
* Describes a single native view tree mutation which may contain
* pointers to an old shadow view, a new shadow view, a parent shadow view and
* final index of inserted or updated view.
* Use static methods to instantiate mutations of different types.
*/
struct ShadowViewMutation final {
using List = std::vector<ShadowViewMutation>;
ShadowViewMutation() = delete;
#pragma mark - Designated Initializers
/**
* Creates and returns an `Create` mutation.
*/
static ShadowViewMutation CreateMutation(ShadowView shadowView);
/**
* Creates and returns an `Delete` mutation.
*/
static ShadowViewMutation DeleteMutation(ShadowView shadowView);
/**
* Creates and returns an `Insert` mutation.
*/
static ShadowViewMutation InsertMutation(Tag parentTag, ShadowView childShadowView, int index);
/**
* Creates and returns an `Insert` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation InsertMutation(const ShadowView &parentShadowView, ShadowView childShadowView, int index)
{
return InsertMutation(parentShadowView.tag, childShadowView, index);
}
/**
* Creates and returns a `Remove` mutation.
*/
static ShadowViewMutation RemoveMutation(Tag parentTag, ShadowView childShadowView, int index);
/**
* Creates and returns a `Remove` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation RemoveMutation(const ShadowView &parentShadowView, ShadowView childShadowView, int index)
{
return RemoveMutation(parentShadowView.tag, childShadowView, index);
}
/**
* Creates and returns an `Update` mutation.
*/
static ShadowViewMutation UpdateMutation(ShadowView oldChildShadowView, ShadowView newChildShadowView, Tag parentTag);
/**
* Creates and returns an `Update` mutation.
* @deprecated Pass parentTag instead of parentShadowView.
*/
static ShadowViewMutation
UpdateMutation(ShadowView oldChildShadowView, ShadowView newChildShadowView, const ShadowView &parentShadowView)
{
return UpdateMutation(oldChildShadowView, newChildShadowView, parentShadowView.tag);
}
#pragma mark - Type
enum Type : std::uint8_t {
Create = 1,
Delete = 2,
Insert = 4,
Remove = 8,
Update = 16,
};
#pragma mark - Fields
Type type = {Create};
Tag parentTag = -1;
ShadowView oldChildShadowView = {};
ShadowView newChildShadowView = {};
int index = -1;
// Some platforms can have the notion of virtual views - views that are in the
// ShadowTree hierarchy but never are on the platform. Generally this is used
// so notify the platform that a view exists so that we can keep EventEmitters
// around, to notify JS of something. This mechanism is DEPRECATED and it is
// highly recommended that you NOT make use of this in your platform!
bool mutatedViewIsVirtual() const;
private:
ShadowViewMutation(Type type, Tag parentTag, ShadowView oldChildShadowView, ShadowView newChildShadowView, int index);
};
using ShadowViewMutationList = std::vector<ShadowViewMutation>;
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const ShadowViewMutation &mutation);
std::vector<DebugStringConvertibleObject> getDebugProps(
const ShadowViewMutation &mutation,
DebugStringConvertibleOptions options);
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,53 @@
/*
* 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 "TelemetryController.h"
#include <react/renderer/mounting/MountingCoordinator.h>
namespace facebook::react {
TelemetryController::TelemetryController(
const MountingCoordinator& mountingCoordinator) noexcept
: mountingCoordinator_(mountingCoordinator) {}
bool TelemetryController::pullTransaction(
const MountingTransactionCallback& willMount,
const MountingTransactionCallback& doMount,
const MountingTransactionCallback& didMount) const {
auto optional = mountingCoordinator_.pullTransaction();
if (!optional.has_value()) {
return false;
}
auto transaction = std::move(*optional);
auto& telemetry = transaction.getTelemetry();
auto numberOfMutations = static_cast<int>(transaction.getMutations().size());
mutex_.lock();
auto compoundTelemetry = compoundTelemetry_;
mutex_.unlock();
willMount(transaction, compoundTelemetry);
telemetry.willMount();
doMount(transaction, compoundTelemetry);
telemetry.didMount();
compoundTelemetry.incorporate(telemetry, numberOfMutations);
didMount(transaction, compoundTelemetry);
mutex_.lock();
compoundTelemetry_ = compoundTelemetry;
mutex_.unlock();
return true;
}
} // namespace facebook::react

View File

@@ -0,0 +1,56 @@
/*
* 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.
*/
#pragma once
#include <functional>
#include <mutex>
#include <react/renderer/mounting/MountingTransaction.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
namespace facebook::react {
class MountingCoordinator;
using MountingTransactionCallback =
std::function<void(const MountingTransaction &transaction, const SurfaceTelemetry &surfaceTelemetry)>;
/*
* Provides convenient tools for aggregating and accessing telemetry data
* associated with running Surface.
*/
class TelemetryController final {
friend class MountingCoordinator;
/*
* To be used by `MountingCoordinator`.
*/
TelemetryController(const MountingCoordinator &mountingCoordinator) noexcept;
/*
* Not copyable.
*/
TelemetryController(const TelemetryController &other) noexcept = delete;
TelemetryController &operator=(const TelemetryController &other) noexcept = delete;
public:
/*
* Calls `MountingCoordinator::pullTransaction()` and aggregates telemetry.
*/
bool pullTransaction(
const MountingTransactionCallback &willMount,
const MountingTransactionCallback &doMount,
const MountingTransactionCallback &didMount) const;
private:
const MountingCoordinator &mountingCoordinator_;
mutable SurfaceTelemetry compoundTelemetry_{};
mutable std::mutex mutex_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,80 @@
/*
* 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 "CullingContext.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include "ShadowViewNodePair.h"
namespace facebook::react {
bool CullingContext::shouldConsiderCulling() const {
return frame.size.width > 0 && frame.size.height > 0;
}
CullingContext CullingContext::adjustCullingContextIfNeeded(
const ShadowViewNodePair& pair) const {
auto cullingContext = *this;
if (ReactNativeFeatureFlags::enableViewCulling()) {
if (auto scrollViewShadowNode =
dynamic_cast<const ScrollViewShadowNode*>(pair.shadowNode)) {
if (scrollViewShadowNode->getConcreteProps().yogaStyle.overflow() !=
yoga::Overflow::Visible &&
!scrollViewShadowNode->getStateData().disableViewCulling) {
auto layoutMetrics = scrollViewShadowNode->getLayoutMetrics();
cullingContext.frame.origin =
-scrollViewShadowNode->getContentOriginOffset(
/* includeTransform */ true);
cullingContext.frame.size =
scrollViewShadowNode->getLayoutMetrics().frame.size;
// Enlarge the frame if an outset ratio is defined
auto outsetRatio = ReactNativeFeatureFlags::viewCullingOutsetRatio();
if (outsetRatio > 0) {
auto xOutset = static_cast<float>(
floor(cullingContext.frame.size.width * outsetRatio));
auto yOutset = static_cast<float>(
floor(cullingContext.frame.size.height * outsetRatio));
cullingContext.frame.origin.x -= xOutset;
cullingContext.frame.origin.y -= yOutset;
cullingContext.frame.size.width += 2.0f * xOutset;
cullingContext.frame.size.height += 2.0f * yOutset;
}
cullingContext.transform = Transform::Identity();
if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
// In RTL, content offset is flipped horizontally.
// We need to flip the culling context frame to match.
// See:
// https://github.com/facebook/react-native/blob/c2f39cfdd87c32b9a59efe8a788b8a03f02b0ea0/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L579
auto stateData = scrollViewShadowNode->getStateData();
cullingContext.frame.origin.x =
stateData.contentBoundingRect.size.width -
layoutMetrics.frame.size.width - cullingContext.frame.origin.x;
}
} else {
cullingContext = {};
}
} else if (pair.shadowView.traits.check(
ShadowNodeTraits::Trait::RootNodeKind)) {
cullingContext = {};
} else {
cullingContext.frame.origin -= pair.shadowView.layoutMetrics.frame.origin;
if (auto layoutableShadowNode =
dynamic_cast<const LayoutableShadowNode*>(pair.shadowNode)) {
cullingContext.transform =
cullingContext.transform * layoutableShadowNode->getTransform();
}
}
}
return cullingContext;
}
} // namespace facebook::react

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/graphics/Rect.h>
#include <react/renderer/graphics/Transform.h>
namespace facebook::react {
struct ShadowViewNodePair;
struct CullingContext {
Rect frame;
Transform transform;
bool shouldConsiderCulling() const;
CullingContext adjustCullingContextIfNeeded(const ShadowViewNodePair &pair) const;
bool operator==(const CullingContext &rhs) const = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/graphics/Point.h>
#include <react/renderer/mounting/ShadowView.h>
namespace facebook::react {
/*
* Describes pair of a `ShadowView` and a `ShadowNode`.
* This is not exposed to the mounting layer.
*/
struct ShadowViewNodePair final {
ShadowView shadowView{};
const ShadowNode *shadowNode;
/**
* The ShadowNode does not form a stacking context, and the native views
* corresponding to its children may be parented to an ancestor.
*/
bool flattened{false};
/**
* Whether this ShadowNode should create a corresponding native view.
*/
bool isConcreteView{true};
Point contextOrigin{0, 0};
size_t mountIndex{0};
/**
* This is nullptr unless `inOtherTree` is set to true.
* We rely on this only for marginal cases. TODO: could we
* rely on this more heavily to simplify the diffing algorithm
* overall?
*/
mutable const ShadowViewNodePair *otherTreePair{nullptr};
/*
* The stored pointer to `ShadowNode` represents an identity of the pair.
*/
bool operator==(const ShadowViewNodePair &rhs) const
{
return this->shadowNode == rhs.shadowNode;
}
bool operator!=(const ShadowViewNodePair &rhs) const
{
return !(*this == rhs);
}
bool inOtherTree() const
{
return this->otherTreePair != nullptr;
}
};
} // namespace facebook::react

View File

@@ -0,0 +1,147 @@
/*
* 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.
*/
#pragma once
#include <algorithm>
#include <utility>
/*
* Extremely simple and naive implementation of a map.
* The map is simple but it's optimized for particular constraints that we have
* here.
*
* A regular map implementation (e.g. `std::unordered_map`) has some basic
* performance guarantees like constant average insertion and lookup complexity.
* This is nice, but it's *average* complexity measured on a non-trivial amount
* of data. The regular map is a very complex data structure that using hashing,
* buckets, multiple comprising operations, multiple allocations and so on.
*
* In our particular case, we need a map for `int` to `void *` with a dozen
* values. In these conditions, nothing can beat a naive implementation using a
* stack-allocated vector. And this implementation is exactly this: no
* allocation, no hashing, no complex branching, no buckets, no iterators, no
* rehashing, no other guarantees. It's crazy limited, unsafe, and performant on
* a trivial amount of data.
*
* Besides that, we also need to optimize for insertion performance (the case
* where a bunch of views appears on the screen first time); in this
* implementation, this is as performant as vector `push_back`.
*/
template <typename KeyT, typename ValueT>
class TinyMap final {
public:
using Pair = std::pair<KeyT, ValueT>;
using Iterator = Pair *;
/**
* This must strictly only be called from outside of this class.
*/
inline Iterator begin()
{
// Force a clean so that iterating over this TinyMap doesn't iterate over
// erased elements. If all elements erased are at the front of the vector,
// then we don't need to clean.
cleanVector(erasedAtFront_ != numErased_);
Iterator it = begin_();
if (it != nullptr) {
return it + erasedAtFront_;
}
return nullptr;
}
inline Iterator end()
{
// `back()` asserts on the vector being non-empty
if (vector_.empty() || numErased_ == vector_.size()) {
return nullptr;
}
return &vector_.back() + 1;
}
inline Iterator find(KeyT key)
{
cleanVector();
react_native_assert(key != 0);
if (begin_() == nullptr) {
return end();
}
for (auto it = begin_() + erasedAtFront_; it != end(); it++) {
if (it->first == key) {
return it;
}
}
return end();
}
inline void insert(Pair pair)
{
react_native_assert(pair.first != 0);
vector_.push_back(pair);
}
inline void erase(Iterator iterator)
{
// Invalidate tag.
iterator->first = 0;
if (iterator == begin_() + erasedAtFront_) {
erasedAtFront_++;
}
numErased_++;
}
private:
/**
* Same as begin() but doesn't call cleanVector at the beginning.
*/
inline Iterator begin_()
{
// `front()` asserts on the vector being non-empty
if (vector_.empty() || vector_.size() == numErased_) {
return nullptr;
}
return &vector_.front();
}
/**
* Remove erased elements from internal vector.
* We only modify the vector if erased elements are at least half of the
* vector.
*/
inline void cleanVector(bool forceClean = false)
{
if ((numErased_ < (vector_.size() / 2) && !forceClean) || vector_.empty() || numErased_ == 0 ||
numErased_ == erasedAtFront_) {
return;
}
if (numErased_ == vector_.size()) {
vector_.clear();
} else {
vector_.erase(
std::remove_if(vector_.begin(), vector_.end(), [](const auto &item) { return item.first == 0; }),
vector_.end());
}
numErased_ = 0;
erasedAtFront_ = 0;
}
std::vector<Pair> vector_;
size_t numErased_{0};
size_t erasedAtFront_{0};
};

View File

@@ -0,0 +1,204 @@
/*
* 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 "sliceChildShadowNodeViewPairs.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include "ShadowViewNodePair.h"
namespace facebook::react {
/*
* Sorting comparator for `reorderInPlaceIfNeeded`.
*/
static bool shouldFirstPairComesBeforeSecondOne(
const ShadowViewNodePair* lhs,
const ShadowViewNodePair* rhs) noexcept {
return lhs->shadowNode->getOrderIndex() < rhs->shadowNode->getOrderIndex();
}
/*
* Reorders pairs in-place based on `orderIndex` using a stable sort algorithm.
*/
static void reorderInPlaceIfNeeded(
std::vector<ShadowViewNodePair*>& pairs) noexcept {
if (pairs.size() < 2) {
return;
}
auto isReorderNeeded = false;
for (const auto& pair : pairs) {
if (pair->shadowNode->getOrderIndex() != 0) {
isReorderNeeded = true;
break;
}
}
if (!isReorderNeeded) {
return;
}
std::stable_sort(
pairs.begin(), pairs.end(), &shouldFirstPairComesBeforeSecondOne);
}
static void sliceChildShadowNodeViewPairsRecursively(
std::vector<ShadowViewNodePair*>& pairList,
size_t& startOfStaticIndex,
ViewNodePairScope& scope,
Point layoutOffset,
const ShadowNode& shadowNode,
const CullingContext& cullingContext) {
for (const auto& sharedChildShadowNode : shadowNode.getChildren()) {
auto& childShadowNode = *sharedChildShadowNode;
// T153547836: Disabled on Android because the mounting infrastructure
// is not fully ready yet.
if (
#ifdef ANDROID
ReactNativeFeatureFlags::useTraitHiddenOnAndroid() &&
#endif
childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) {
continue;
}
auto shadowView = ShadowView(childShadowNode);
if (ReactNativeFeatureFlags::enableViewCulling()) {
auto isViewCullable =
!shadowView.traits.check(
ShadowNodeTraits::Trait::Unstable_uncullableView) &&
!shadowView.traits.check(
ShadowNodeTraits::Trait::Unstable_uncullableTrace);
if (cullingContext.shouldConsiderCulling() && isViewCullable) {
auto overflowInsetFrame =
shadowView.layoutMetrics.getOverflowInsetFrame() *
cullingContext.transform;
if (auto layoutableShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&childShadowNode)) {
overflowInsetFrame =
overflowInsetFrame * layoutableShadowNode->getTransform();
}
// Embedded Text components can have an empty layout, while these still
// need to be mounted to set the correct react tags on the text
// fragments. These should not be culled.
auto hasLayout = overflowInsetFrame.size.width > 0 ||
overflowInsetFrame.size.height > 0;
auto doesIntersect =
Rect::intersect(cullingContext.frame, overflowInsetFrame) != Rect{};
if (hasLayout && !doesIntersect) {
continue; // Culling.
}
}
}
auto origin = layoutOffset;
auto cullingContextCopy = cullingContext.adjustCullingContextIfNeeded(
{.shadowView = shadowView, .shadowNode = &childShadowNode});
if (shadowView.layoutMetrics != EmptyLayoutMetrics) {
origin += shadowView.layoutMetrics.frame.origin;
shadowView.layoutMetrics.frame.origin += layoutOffset;
}
// This might not be a FormsView, or a FormsStackingContext. We let the
// differ handle removal of flattened views from the Mounting layer and
// shuffling their children around.
bool childrenFormStackingContexts = shadowNode.getTraits().check(
ShadowNodeTraits::Trait::ChildrenFormStackingContext);
bool isConcreteView = (childShadowNode.getTraits().check(
ShadowNodeTraits::Trait::FormsView) ||
childrenFormStackingContexts) &&
!childShadowNode.getTraits().check(
ShadowNodeTraits::Trait::ForceFlattenView);
bool areChildrenFlattened =
(!childShadowNode.getTraits().check(
ShadowNodeTraits::Trait::FormsStackingContext) &&
!childrenFormStackingContexts) ||
childShadowNode.getTraits().check(
ShadowNodeTraits::Trait::ForceFlattenView);
Point storedOrigin = {};
if (areChildrenFlattened) {
storedOrigin = origin;
}
scope.push_back(
{shadowView,
&childShadowNode,
areChildrenFlattened,
isConcreteView,
storedOrigin});
if (shadowView.layoutMetrics.positionType == PositionType::Static) {
auto it = pairList.begin();
std::advance(it, startOfStaticIndex);
pairList.insert(it, &scope.back());
startOfStaticIndex++;
if (areChildrenFlattened) {
sliceChildShadowNodeViewPairsRecursively(
pairList,
startOfStaticIndex,
scope,
origin,
childShadowNode,
cullingContextCopy);
}
} else {
pairList.push_back(&scope.back());
if (areChildrenFlattened) {
size_t pairListSize = pairList.size();
sliceChildShadowNodeViewPairsRecursively(
pairList,
pairListSize,
scope,
origin,
childShadowNode,
cullingContextCopy);
}
}
}
}
std::vector<ShadowViewNodePair*> sliceChildShadowNodeViewPairs(
const ShadowViewNodePair& shadowNodePair,
ViewNodePairScope& scope,
bool allowFlattened,
Point layoutOffset,
const CullingContext& cullingContext) {
const auto& shadowNode = *shadowNodePair.shadowNode;
auto pairList = std::vector<ShadowViewNodePair*>{};
if (shadowNodePair.flattened && shadowNodePair.isConcreteView &&
!allowFlattened) {
return pairList;
}
size_t startOfStaticIndex = 0;
sliceChildShadowNodeViewPairsRecursively(
pairList,
startOfStaticIndex,
scope,
layoutOffset,
shadowNode,
cullingContext);
// Sorting pairs based on `orderIndex` if needed.
reorderInPlaceIfNeeded(pairList);
// Set list and mountIndex for each after reordering
size_t mountIndex = 0;
for (auto child : pairList) {
child->mountIndex =
(child->isConcreteView ? mountIndex++ : static_cast<unsigned long>(-1));
}
return pairList;
}
} // namespace facebook::react

View File

@@ -0,0 +1,53 @@
/*
* 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.
*/
#pragma once
#include <deque>
#include "CullingContext.h"
namespace facebook::react {
struct ShadowViewNodePair;
/**
* During differ, we need to keep some `ShadowViewNodePair`s in memory.
* Some `ShadowViewNodePair`s are referenced from std::vectors returned
* by `sliceChildShadowNodeViewPairs`; some are referenced in TinyMaps
* for view (un)flattening especially; and it is not always clear which
* std::vectors will outlive which TinyMaps, and vice-versa, so it doesn't
* make sense for the std::vector or TinyMap to own any `ShadowViewNodePair`s.
*
* Thus, we introduce the concept of a scope.
*
* For the duration of some operation, we keep a ViewNodePairScope around, such
* that: (1) the ViewNodePairScope keeps each
* ShadowViewNodePair alive, (2) we have a stable pointer value that we can
* use to reference each ShadowViewNodePair (not guaranteed with std::vector,
* for example, which may have to resize and move values around).
*
* As long as we only manipulate the data-structure with push_back, std::deque
* both (1) ensures that pointers into the data-structure are never invalidated,
* and (2) tries to efficiently allocate storage such that as many objects as
* possible are close in memory, but does not guarantee adjacency.
*/
using ViewNodePairScope = std::deque<ShadowViewNodePair>;
/**
* Generates a list of `ShadowViewNodePair`s that represents a layer of a
* flattened view hierarchy. The V2 version preserves nodes even if they do
* not form views and their children are flattened.
*/
std::vector<ShadowViewNodePair *> sliceChildShadowNodeViewPairs(
const ShadowViewNodePair &shadowNodePair,
ViewNodePairScope &viewNodePairScope,
bool allowFlattened,
Point layoutOffset,
const CullingContext &cullingContext);
} // namespace facebook::react

View File

@@ -0,0 +1,100 @@
/*
* 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 "StubView.h"
#ifdef STUB_VIEW_TREE_VERBOSE
#include <glog/logging.h>
#endif
namespace facebook::react {
StubView::operator ShadowView() const {
auto shadowView = ShadowView{};
shadowView.componentName = componentName;
shadowView.componentHandle = componentHandle;
shadowView.surfaceId = surfaceId;
shadowView.tag = tag;
shadowView.props = props;
shadowView.eventEmitter = eventEmitter;
shadowView.layoutMetrics = layoutMetrics;
shadowView.state = state;
shadowView.traits = traits;
return shadowView;
}
void StubView::update(const ShadowView& shadowView) {
componentName = shadowView.componentName;
componentHandle = shadowView.componentHandle;
surfaceId = shadowView.surfaceId;
tag = shadowView.tag;
props = shadowView.props;
eventEmitter = shadowView.eventEmitter;
layoutMetrics = shadowView.layoutMetrics;
state = shadowView.state;
traits = shadowView.traits;
}
bool operator==(const StubView& lhs, const StubView& rhs) {
if (lhs.props != rhs.props) {
#ifdef STUB_VIEW_TREE_VERBOSE
LOG(ERROR) << "StubView: props do not match. lhs hash: "
<< std::hash<ShadowView>{}((ShadowView)lhs)
<< " rhs hash: " << std::hash<ShadowView>{}((ShadowView)rhs);
#endif
return false;
}
if (lhs.layoutMetrics != rhs.layoutMetrics) {
#ifdef STUB_VIEW_TREE_VERBOSE
LOG(ERROR) << "StubView: layoutMetrics do not match lhs hash: "
<< std::hash<ShadowView>{}((ShadowView)lhs)
<< " rhs hash: " << std::hash<ShadowView>{}((ShadowView)rhs);
#endif
return false;
}
return true;
}
bool operator!=(const StubView& lhs, const StubView& rhs) {
return !(lhs == rhs);
}
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const StubView& stubView) {
return std::string{"Stub"} +
std::string{
stubView.componentHandle != 0 ? stubView.componentName : "[invalid]"};
}
std::vector<DebugStringConvertibleObject> getDebugProps(
const StubView& stubView,
DebugStringConvertibleOptions options) {
return {
{"surfaceId", getDebugDescription(stubView.surfaceId, options)},
{"tag", getDebugDescription(stubView.tag, options)},
{"props", getDebugDescription(stubView.props, options)},
{"eventEmitter", getDebugDescription(stubView.eventEmitter, options)},
{"layoutMetrics", getDebugDescription(stubView.layoutMetrics, options)},
{"state", getDebugDescription(stubView.state, options)},
};
}
std::vector<StubView> getDebugChildren(
const StubView& stubView,
DebugStringConvertibleOptions /*options*/) {
std::vector<StubView> result;
result.reserve(stubView.children.size());
for (const auto& child : stubView.children) {
result.push_back(*child);
}
return result;
}
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,60 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <vector>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/State.h>
#include <react/renderer/debug/debugStringConvertibleUtils.h>
#include <react/renderer/mounting/ShadowView.h>
namespace facebook::react {
static const int NO_VIEW_TAG = -1;
class StubView final {
public:
using Shared = std::shared_ptr<StubView>;
StubView() = default;
StubView(const StubView &stubView) = default;
operator ShadowView() const;
void update(const ShadowView &shadowView);
ComponentName componentName;
ComponentHandle componentHandle;
SurfaceId surfaceId;
Tag tag;
ShadowNodeTraits traits{};
Props::Shared props;
SharedEventEmitter eventEmitter;
LayoutMetrics layoutMetrics;
State::Shared state;
std::vector<StubView::Shared> children;
Tag parentTag{NO_VIEW_TAG};
};
bool operator==(const StubView &lhs, const StubView &rhs);
bool operator!=(const StubView &lhs, const StubView &rhs);
#if RN_DEBUG_STRING_CONVERTIBLE
std::string getDebugName(const StubView &stubView);
std::vector<DebugStringConvertibleObject> getDebugProps(
const StubView &stubView,
DebugStringConvertibleOptions options);
std::vector<StubView> getDebugChildren(const StubView &stubView, DebugStringConvertibleOptions options);
#endif
} // namespace facebook::react

View File

@@ -0,0 +1,390 @@
/*
* 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 "StubViewTree.h"
#include <folly/json.h>
#include <glog/logging.h>
#include <react/debug/react_native_assert.h>
#include <sstream>
#ifdef STUB_VIEW_TREE_VERBOSE
#define STUB_VIEW_LOG(code) code
#else
#define STUB_VIEW_LOG(code)
#endif
namespace facebook::react {
namespace {
std::string getComponentName(const ShadowView& shadowView) {
return std::string{"\""} + shadowView.componentName + "\"";
}
} // namespace
StubViewTree::StubViewTree(const ShadowView& shadowView)
: rootTag_(shadowView.tag) {
auto view = std::make_shared<StubView>();
view->update(shadowView);
registry_[shadowView.tag] = view;
}
const StubView& StubViewTree::getRootStubView() const {
return *registry_.at(rootTag_);
}
const StubView& StubViewTree::getStubView(Tag tag) const {
return *registry_.at(tag);
}
size_t StubViewTree::size() const {
return registry_.size();
}
void StubViewTree::mutate(const ShadowViewMutationList& mutations) {
STUB_VIEW_LOG({ LOG(ERROR) << "StubView: Mutating Begin"; });
for (const auto& mutation : mutations) {
switch (mutation.type) {
case ShadowViewMutation::Create: {
react_native_assert(mutation.parentTag == -1);
react_native_assert(mutation.oldChildShadowView == ShadowView{});
react_native_assert(mutation.newChildShadowView.props);
auto stubView = std::make_shared<StubView>();
stubView->update(mutation.newChildShadowView);
auto tag = mutation.newChildShadowView.tag;
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Create [" << tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)*stubView);
});
if (hasTag(tag)) {
LOG(ERROR) << "StubView: Create [" << tag << "]: tag already exists"
<< (tag == rootTag_ ? " (and it's the root tag)" : "")
<< ". The current registry: ";
dumpTags(LOG(ERROR));
}
react_native_assert(!hasTag(tag));
registry_[tag] = stubView;
recordMutation(mutation);
break;
}
case ShadowViewMutation::Delete: {
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Delete [" << mutation.oldChildShadowView.tag
<< "] ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView);
});
react_native_assert(mutation.parentTag == -1);
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto tag = mutation.oldChildShadowView.tag;
react_native_assert(hasTag(tag));
auto stubView = registry_[tag];
registry_.erase(tag);
recordMutation(mutation);
break;
}
case ShadowViewMutation::Insert: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.oldChildShadowView == ShadowView{});
auto parentTag = mutation.parentTag;
auto childTag = mutation.newChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: INSERT mutation assertion failure: parentTag not found: ["
<< parentTag << "] inserting child: [" << childTag << "]";
}
if (!hasTag(childTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: INSERT mutation assertion failure: childTag not found: ["
<< parentTag << "] inserting child: [" << childTag << "]";
}
react_native_assert(hasTag(parentTag));
auto parentStubView = registry_[parentTag];
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
childStubView->update(mutation.newChildShadowView);
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Insert [" << childTag << "] into ["
<< parentTag << "] @" << mutation.index << "("
<< parentStubView->children.size() << " children)";
});
react_native_assert(childStubView->parentTag == NO_VIEW_TAG);
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >=
static_cast<size_t>(mutation.index));
childStubView->parentTag = parentTag;
parentStubView->children.insert(
parentStubView->children.begin() + mutation.index, childStubView);
} else {
auto childTag = mutation.newChildShadowView.tag;
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
childStubView->update(mutation.newChildShadowView);
}
recordMutation(mutation);
break;
}
case ShadowViewMutation::Remove: {
if (!mutation.mutatedViewIsVirtual()) {
react_native_assert(mutation.newChildShadowView == ShadowView{});
auto parentTag = mutation.parentTag;
auto childTag = mutation.oldChildShadowView.tag;
if (!hasTag(parentTag)) {
LOG(ERROR)
<< "StubView: ASSERT FAILURE: REMOVE mutation assertion failure: parentTag not found: ["
<< parentTag << "] removing child: [" << childTag << "]";
}
react_native_assert(hasTag(parentTag));
auto parentStubView = registry_[parentTag];
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Remove [" << childTag << "] from ["
<< parentTag << "] @" << mutation.index << " with "
<< parentStubView->children.size() << " children";
});
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >
static_cast<size_t>(mutation.index));
react_native_assert(hasTag(childTag));
auto childStubView = registry_[childTag];
react_native_assert(childStubView->parentTag == parentTag);
STUB_VIEW_LOG({
std::string strChildList = "";
int i = 0;
for (const auto& child : parentStubView->children) {
strChildList.append(std::to_string(i));
strChildList.append(":");
strChildList.append(std::to_string(child->tag));
strChildList.append(", ");
i++;
}
LOG(ERROR) << "StubView: BEFORE REMOVE: Children of " << parentTag
<< ": " << strChildList;
});
react_native_assert(
mutation.index >= 0 &&
parentStubView->children.size() >
static_cast<size_t>(mutation.index) &&
parentStubView->children[mutation.index]->tag ==
childStubView->tag);
childStubView->parentTag = NO_VIEW_TAG;
parentStubView->children.erase(
parentStubView->children.begin() + mutation.index);
}
recordMutation(mutation);
break;
}
case ShadowViewMutation::Update: {
STUB_VIEW_LOG({
LOG(ERROR) << "StubView: Update [" << mutation.newChildShadowView.tag
<< "] old hash: ##"
<< std::hash<ShadowView>{}(mutation.oldChildShadowView)
<< " new hash: ##"
<< std::hash<ShadowView>{}(mutation.newChildShadowView);
});
react_native_assert(mutation.oldChildShadowView.tag != 0);
react_native_assert(mutation.newChildShadowView.tag != 0);
react_native_assert(mutation.newChildShadowView.props);
react_native_assert(
mutation.newChildShadowView.tag == mutation.oldChildShadowView.tag);
react_native_assert(hasTag(mutation.newChildShadowView.tag));
auto oldStubView = registry_[mutation.newChildShadowView.tag];
react_native_assert(oldStubView->tag != 0);
if (mutation.parentTag != 0) {
react_native_assert(hasTag(mutation.parentTag));
react_native_assert(oldStubView->parentTag == mutation.parentTag);
}
oldStubView->update(mutation.newChildShadowView);
// Hash for stub view and the ShadowView should be identical - this
// tests that StubView and ShadowView hash are equivalent.
react_native_assert(
std::hash<ShadowView>{}((ShadowView)*oldStubView) ==
std::hash<ShadowView>{}(mutation.newChildShadowView));
recordMutation(mutation);
break;
}
}
}
STUB_VIEW_LOG({ LOG(ERROR) << "StubView: Mutating End"; });
// For iOS especially: flush logs because some might be lost on iOS if an
// assert is hit right after this.
google::FlushLogFiles(google::GLOG_INFO);
}
void StubViewTree::dispatchCommand(
const ShadowView& shadowView,
const std::string& commandName,
const folly::dynamic& args) {
auto stream = std::ostringstream();
stream << "Command {type: " << getComponentName(shadowView)
<< ", nativeID: " << getNativeId(shadowView) << ", name: \""
<< commandName;
if (!args.empty()) {
stream << ", args: " << folly::toJson(args);
}
stream << "\"}";
mountingLogs_.push_back(std::string(stream.str()));
}
std::vector<std::string> StubViewTree::takeMountingLogs() {
auto logs = mountingLogs_;
mountingLogs_.clear();
return logs;
}
std::ostream& StubViewTree::dumpTags(std::ostream& stream) const {
for (const auto& pair : registry_) {
auto& stubView = *pair.second;
stream << "[" << stubView.tag << "]##"
<< std::hash<ShadowView>{}((ShadowView)stubView) << " ";
}
return stream;
}
bool operator==(const StubViewTree& lhs, const StubViewTree& rhs) {
if (lhs.registry_.size() != rhs.registry_.size()) {
STUB_VIEW_LOG({
LOG(ERROR) << "Registry sizes are different. Sizes: LHS: "
<< lhs.registry_.size() << " RHS: " << rhs.registry_.size();
LOG(ERROR) << "Tags in LHS: ";
lhs.dumpTags(LOG(ERROR));
LOG(ERROR) << "Tags in RHS: ";
rhs.dumpTags(LOG(ERROR));
});
return false;
}
for (const auto& pair : lhs.registry_) {
auto& lhsStubView = *lhs.registry_.at(pair.first);
auto& rhsStubView = *rhs.registry_.at(pair.first);
if (lhsStubView != rhsStubView) {
STUB_VIEW_LOG({
LOG(ERROR) << "Registry entries are different. LHS: ["
<< lhsStubView.tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)lhsStubView)
<< " RHS: [" << rhsStubView.tag << "] ##"
<< std::hash<ShadowView>{}((ShadowView)rhsStubView);
});
return false;
}
}
return true;
}
bool operator!=(const StubViewTree& lhs, const StubViewTree& rhs) {
return !(lhs == rhs);
}
void StubViewTree::recordMutation(const ShadowViewMutation& mutation) {
switch (mutation.type) {
case ShadowViewMutation::Create: {
auto stream = std::ostringstream();
stream << "Create {type: "
<< getComponentName(mutation.newChildShadowView)
<< ", nativeID: " << getNativeId(mutation.newChildShadowView)
<< "}";
mountingLogs_.push_back(std::string(stream.str()));
break;
}
case ShadowViewMutation::Delete: {
auto stream = std::ostringstream();
stream << "Delete {type: "
<< getComponentName(mutation.oldChildShadowView)
<< ", nativeID: " << getNativeId(mutation.oldChildShadowView)
<< "}";
mountingLogs_.push_back(std::string(stream.str()));
break;
}
case ShadowViewMutation::Insert: {
auto stream = std::ostringstream();
stream << "Insert {type: "
<< getComponentName(mutation.newChildShadowView)
<< ", parentNativeID: " << getNativeId(mutation.parentTag)
<< ", index: " << mutation.index
<< ", nativeID: " << getNativeId(mutation.newChildShadowView)
<< "}";
mountingLogs_.push_back(std::string(stream.str()));
break;
}
case ShadowViewMutation::Remove: {
auto stream = std::ostringstream();
stream << "Remove {type: "
<< getComponentName(mutation.oldChildShadowView)
<< ", parentNativeID: " << getNativeId(mutation.parentTag)
<< ", index: " << mutation.index
<< ", nativeID: " << getNativeId(mutation.oldChildShadowView)
<< "}";
mountingLogs_.push_back(std::string(stream.str()));
break;
}
case ShadowViewMutation::Update: {
auto stream = std::ostringstream();
stream << "Update {type: "
<< getComponentName(mutation.newChildShadowView)
<< ", nativeID: " << getNativeId(mutation.newChildShadowView)
<< "}";
mountingLogs_.push_back(std::string(stream.str()));
break;
}
}
}
std::string StubViewTree::getNativeId(Tag tag) {
auto& view = getStubView(tag);
return getNativeId(view);
}
std::string StubViewTree::getNativeId(const ShadowView& shadowView) {
if (shadowView.traits.check(ShadowNodeTraits::Trait::RootNodeKind)) {
return "(root)";
}
if (shadowView.props->nativeId.empty()) {
return "(N/A)";
}
return "\"" + shadowView.props->nativeId + "\"";
}
} // namespace facebook::react

View File

@@ -0,0 +1,67 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <unordered_map>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs/StubView.h>
namespace facebook::react {
class StubViewTree {
public:
StubViewTree() = default;
StubViewTree(const ShadowView &shadowView);
void mutate(const ShadowViewMutationList &mutations);
void dispatchCommand(const ShadowView &shadowView, const std::string &commandName, const folly::dynamic &args);
const StubView &getRootStubView() const;
/*
* Returns a view with given tag.
*/
const StubView &getStubView(Tag tag) const;
/*
* Returns the total amount of views in the tree.
*/
size_t size() const;
/**
* Returns the list of mounting operations in the buffer and clears it.
*/
std::vector<std::string> takeMountingLogs();
bool hasTag(Tag tag) const
{
return registry_.find(tag) != registry_.end();
}
private:
Tag rootTag_{};
std::unordered_map<Tag, StubView::Shared> registry_{};
std::vector<std::string> mountingLogs_{};
friend bool operator==(const StubViewTree &lhs, const StubViewTree &rhs);
friend bool operator!=(const StubViewTree &lhs, const StubViewTree &rhs);
std::ostream &dumpTags(std::ostream &stream) const;
std::string getNativeId(Tag tag);
std::string getNativeId(const ShadowView &shadowView);
void recordMutation(const ShadowViewMutation &mutation);
};
bool operator==(const StubViewTree &lhs, const StubViewTree &rhs);
bool operator!=(const StubViewTree &lhs, const StubViewTree &rhs);
} // namespace facebook::react

View File

@@ -0,0 +1,111 @@
/*
* 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 "stubs.h"
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/core/ShadowNodeFragment.h>
#include <react/renderer/mounting/Differentiator.h>
#include "../internal/ShadowViewNodePair.h"
#include "../internal/sliceChildShadowNodeViewPairs.h"
namespace facebook::react {
/*
* Sorting comparator for `reorderInPlaceIfNeeded`.
*/
static bool shouldFirstPairComesBeforeSecondOne(
const ShadowViewNodePair* lhs,
const ShadowViewNodePair* rhs) noexcept {
return lhs->shadowNode->getOrderIndex() < rhs->shadowNode->getOrderIndex();
}
/*
* Reorders pairs in-place based on `orderIndex` using a stable sort algorithm.
*/
static void reorderInPlaceIfNeeded(
std::vector<ShadowViewNodePair*>& pairs) noexcept {
// This is a simplified version of the function intentionally copied from
// `Differentiator.cpp`.
std::stable_sort(
pairs.begin(), pairs.end(), &shouldFirstPairComesBeforeSecondOne);
}
/*
* Generates `create` and `insert` instructions recursively traversing a shadow
* tree.
* This is a trivial implementation of diffing algorithm that can only "diff"
* an empty tree with some other one.
*/
static void calculateShadowViewMutationsForNewTree(
ShadowViewMutation::List& mutations,
ViewNodePairScope& scope,
const ShadowView& parentShadowView,
std::vector<ShadowViewNodePair*> newChildPairs) {
// Sorting pairs based on `orderIndex` if needed.
reorderInPlaceIfNeeded(newChildPairs);
for (auto newChildPair : newChildPairs) {
if (!newChildPair->isConcreteView) {
continue;
}
mutations.push_back(
ShadowViewMutation::CreateMutation(newChildPair->shadowView));
mutations.push_back(
ShadowViewMutation::InsertMutation(
parentShadowView.tag,
newChildPair->shadowView,
static_cast<int>(newChildPair->mountIndex)));
auto newGrandChildPairs =
sliceChildShadowNodeViewPairs(*newChildPair, scope, false, {}, {});
calculateShadowViewMutationsForNewTree(
mutations, scope, newChildPair->shadowView, newGrandChildPairs);
}
}
StubViewTree buildStubViewTreeWithoutUsingDifferentiator(
const ShadowNode& rootShadowNode) {
auto mutations = ShadowViewMutation::List{};
mutations.reserve(256);
ViewNodePairScope scope;
ShadowViewNodePair rootShadowNodePair{.shadowNode = &rootShadowNode};
calculateShadowViewMutationsForNewTree(
mutations,
scope,
ShadowView(rootShadowNode),
sliceChildShadowNodeViewPairs(rootShadowNodePair, scope, false, {}, {}));
auto emptyRootShadowNode = rootShadowNode.clone(
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = ShadowNode::emptySharedShadowNodeSharedList()});
auto stubViewTree = StubViewTree(ShadowView(*emptyRootShadowNode));
stubViewTree.mutate(mutations);
return stubViewTree;
}
StubViewTree buildStubViewTreeUsingDifferentiator(
const ShadowNode& rootShadowNode) {
auto emptyRootShadowNode = rootShadowNode.clone(
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = ShadowNode::emptySharedShadowNodeSharedList()});
auto mutations =
calculateShadowViewMutations(*emptyRootShadowNode, rootShadowNode);
auto stubViewTree = StubViewTree(ShadowView(*emptyRootShadowNode));
stubViewTree.mutate(mutations);
return stubViewTree;
}
} // namespace facebook::react

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/stubs/StubView.h>
#include <react/renderer/mounting/stubs/StubViewTree.h>
namespace facebook::react {
/*
* Builds a ShadowView tree from given root ShadowNode using custom built-in
* implementation (*without* using Differentiator).
*/
StubViewTree buildStubViewTreeWithoutUsingDifferentiator(const ShadowNode &rootShadowNode);
/*
* Builds a ShadowView tree from given root ShadowNode using Differentiator by
* generating mutation instructions between empty and final trees.
*/
StubViewTree buildStubViewTreeUsingDifferentiator(const ShadowNode &rootShadowNode);
} // namespace facebook::react

View File

@@ -0,0 +1,247 @@
/*
* 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 <memory>
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs/stubs.h>
namespace facebook::react {
class OrderIndexTest : public ::testing::Test {
protected:
std::unique_ptr<ComponentBuilder> builder_;
std::shared_ptr<RootShadowNode> rootShadowNode_;
std::shared_ptr<ViewShadowNode> nodeA_;
std::shared_ptr<ViewShadowNode> nodeB_;
std::shared_ptr<ViewShadowNode> nodeC_;
std::shared_ptr<ViewShadowNode> nodeD_;
std::shared_ptr<RootShadowNode> currentRootShadowNode_;
StubViewTree currentStubViewTree_;
void SetUp() override {
builder_ = std::make_unique<ComponentBuilder>(simpleComponentBuilder());
auto element = Element<RootShadowNode>()
.reference(rootShadowNode_)
.tag(1)
.children({
Element<ViewShadowNode>().tag(2).reference(nodeA_),
Element<ViewShadowNode>().tag(3).reference(nodeB_),
Element<ViewShadowNode>().tag(4).reference(nodeC_),
Element<ViewShadowNode>().tag(5).reference(nodeD_),
});
builder_->build(element);
mutateViewShadowNodeProps_(nodeA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeC_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.backgroundColor = blackColor(); // to ensure it won't get flattened
});
currentRootShadowNode_ = rootShadowNode_;
currentRootShadowNode_->layoutIfNeeded();
currentStubViewTree_ =
buildStubViewTreeWithoutUsingDifferentiator(*currentRootShadowNode_);
}
void mutateViewShadowNodeProps_(
const std::shared_ptr<ViewShadowNode>& node,
std::function<void(ViewProps& props)> callback) {
rootShadowNode_ =
std::static_pointer_cast<RootShadowNode>(rootShadowNode_->cloneTree(
node->getFamily(), [&](const ShadowNode& oldShadowNode) {
auto viewProps = std::make_shared<ViewShadowNodeProps>();
callback(*viewProps);
return oldShadowNode.clone(
ShadowNodeFragment{.props = viewProps});
}));
}
void testViewTree_(
const std::function<void(const StubViewTree& viewTree)>& callback) {
rootShadowNode_->layoutIfNeeded();
callback(buildStubViewTreeUsingDifferentiator(*rootShadowNode_));
auto mutations =
calculateShadowViewMutations(*currentRootShadowNode_, *rootShadowNode_);
currentRootShadowNode_ = rootShadowNode_;
currentStubViewTree_.mutate(mutations);
callback(currentStubViewTree_);
}
};
TEST_F(OrderIndexTest, defaultOrderIsDocumentOrder) {
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeD_->getTag());
});
}
TEST_F(OrderIndexTest, basicZIndex) {
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = 10; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = 1; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 2; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeB_->getTag());
});
}
TEST_F(OrderIndexTest, negativeZIndex) {
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = -10; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = -1; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 2; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeA_->getTag());
});
}
TEST_F(OrderIndexTest, zeroZIndex) {
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = 0; });
mutateViewShadowNodeProps_(
nodeD_, [](ViewProps& props) { props.zIndex = 0; });
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeD_->getTag());
});
}
TEST_F(OrderIndexTest, staticBehindNonStatic) {
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeC_->getTag());
});
}
TEST_F(OrderIndexTest, zIndexStaticBehindNonStatic) {
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.zIndex = 5; });
mutateViewShadowNodeProps_(
nodeC_, [](ViewProps& props) { props.zIndex = -1; });
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeC_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeB_->getTag());
});
}
TEST_F(OrderIndexTest, staticDoesNotGetZIndex) {
mutateViewShadowNodeProps_(nodeB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
props.zIndex = 5;
});
mutateViewShadowNodeProps_(nodeD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.backgroundColor = blackColor();
props.zIndex = -5;
});
testViewTree_([this](const StubViewTree& viewTree) {
EXPECT_EQ(viewTree.size(), 5);
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, nodeB_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, nodeD_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, nodeA_->getTag());
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, nodeC_->getTag());
});
}
} // namespace facebook::react

View File

@@ -0,0 +1,437 @@
/*
* 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 <vector>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs/stubs.h>
#include <react/test_utils/Entropy.h>
#include <react/test_utils/shadowTreeGeneration.h>
// Uncomment when random test blocks are uncommented below.
// #include <algorithm>
// #include <random>
namespace facebook::react {
static void testShadowNodeTreeLifeCycle(
uint_fast32_t seed,
int treeSize,
int repeats,
int stages) {
auto entropy = seed == 0 ? Entropy() : Entropy(seed);
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
auto componentDescriptorParameters = ComponentDescriptorParameters{
.eventDispatcher = eventDispatcher,
.contextContainer = contextContainer,
.flavor = nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
PropsParserContext parserContext{-1, *contextContainer};
auto allNodes = std::vector<std::shared_ptr<const ShadowNode>>{};
for (int i = 0; i < repeats; i++) {
allNodes.clear();
auto family = rootComponentDescriptor.createFamily(
{.tag = Tag(1), .surfaceId = SurfaceId(1), .instanceHandle = nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{
.props = RootShadowNode::defaultSharedProps()},
family)));
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
.minimumSize = Size{.width = 512, .height = 0},
.maximumSize =
Size{
.width = 512,
.height = std::numeric_limits<Float>::infinity()}},
LayoutContext{});
// Generation of a random tree.
auto singleRootChildNode =
generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize);
// Injecting a tree into the root node.
auto currentRootNode = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = std::make_shared<
std::vector<std::shared_ptr<const ShadowNode>>>(
std::vector<std::shared_ptr<const ShadowNode>>{
singleRootChildNode})}));
// Building an initial view hierarchy.
auto viewTree = StubViewTree(ShadowView(*emptyRootNode));
viewTree.mutate(
calculateShadowViewMutations(*emptyRootNode, *currentRootNode));
for (int j = 0; j < stages; j++) {
auto nextRootNode = currentRootNode;
// Mutating the tree.
alterShadowTree(
entropy,
nextRootNode,
{
&messWithChildren,
&messWithYogaStyles,
&messWithLayoutableOnlyFlag,
});
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
// Laying out the tree.
std::const_pointer_cast<RootShadowNode>(nextRootNode)
->layoutIfNeeded(&affectedLayoutableNodes);
nextRootNode->sealRecursive();
allNodes.push_back(nextRootNode);
// Calculating mutations.
auto mutations =
calculateShadowViewMutations(*currentRootNode, *nextRootNode);
// Make sure that in a single frame, a DELETE for a
// view is not followed by a CREATE for the same view.
{
std::vector<int> deletedTags{};
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Delete) {
deletedTags.push_back(mutation.oldChildShadowView.tag);
}
}
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Create) {
if (std::find(
deletedTags.begin(),
deletedTags.end(),
mutation.newChildShadowView.tag) != deletedTags.end()) {
LOG(ERROR) << "Deleted tag was recreated in mutations list: ["
<< mutation.newChildShadowView.tag << "]";
react_native_assert(false);
}
}
}
}
// Mutating the view tree.
viewTree.mutate(mutations);
// Building a view tree to compare with.
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR) << "Entropy seed: " << entropy.getSeed() << "\n";
// There are some issues getting `getDebugDescription` to compile
// under test on Android for now.
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "Shadow Tree before: \n"
<< currentRootNode->getDebugDescription();
LOG(ERROR) << "Shadow Tree after: \n"
<< nextRootNode->getDebugDescription();
LOG(ERROR) << "View Tree before: \n"
<< getDebugDescription(viewTree.getRootStubView(), {});
LOG(ERROR) << "View Tree after: \n"
<< getDebugDescription(
rebuiltViewTree.getRootStubView(), {});
LOG(ERROR) << "Mutations:" << "\n"
<< getDebugDescription(mutations, {});
#endif
react_native_assert(false);
}
currentRootNode = nextRootNode;
}
}
SUCCEED();
}
static void testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
uint_fast32_t seed,
int treeSize,
int repeats,
int stages) {
auto entropy = seed == 0 ? Entropy() : Entropy(seed);
auto eventDispatcher = EventDispatcher::Shared{};
auto contextContainer = std::make_shared<ContextContainer>();
auto componentDescriptorParameters = ComponentDescriptorParameters{
.eventDispatcher = eventDispatcher,
.contextContainer = contextContainer,
.flavor = nullptr};
auto viewComponentDescriptor =
ViewComponentDescriptor(componentDescriptorParameters);
auto rootComponentDescriptor =
RootComponentDescriptor(componentDescriptorParameters);
PropsParserContext parserContext{-1, *contextContainer};
auto allNodes = std::vector<std::shared_ptr<const ShadowNode>>{};
for (int i = 0; i < repeats; i++) {
allNodes.clear();
auto family = rootComponentDescriptor.createFamily(
{.tag = Tag(1), .surfaceId = SurfaceId(1), .instanceHandle = nullptr});
// Creating an initial root shadow node.
auto emptyRootNode = std::const_pointer_cast<RootShadowNode>(
std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{
.props = RootShadowNode::defaultSharedProps()},
family)));
// Applying size constraints.
emptyRootNode = emptyRootNode->clone(
parserContext,
LayoutConstraints{
.minimumSize = Size{.width = 512, .height = 0},
.maximumSize =
Size{
.width = 512,
.height = std::numeric_limits<Float>::infinity()}},
LayoutContext{});
// Generation of a random tree.
auto singleRootChildNode =
generateShadowNodeTree(entropy, viewComponentDescriptor, treeSize);
// Injecting a tree into the root node.
auto currentRootNode = std::static_pointer_cast<const RootShadowNode>(
emptyRootNode->ShadowNode::clone(
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = std::make_shared<
std::vector<std::shared_ptr<const ShadowNode>>>(
std::vector<std::shared_ptr<const ShadowNode>>{
singleRootChildNode})}));
// Building an initial view hierarchy.
auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*emptyRootNode);
viewTree.mutate(
calculateShadowViewMutations(*emptyRootNode, *currentRootNode));
for (int j = 0; j < stages; j++) {
auto nextRootNode = currentRootNode;
// Mutating the tree.
alterShadowTree(
entropy,
nextRootNode,
{
&messWithYogaStyles,
&messWithLayoutableOnlyFlag,
});
alterShadowTree(entropy, nextRootNode, &messWithNodeFlattenednessFlags);
alterShadowTree(entropy, nextRootNode, &messWithChildren);
std::vector<const LayoutableShadowNode*> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
// Laying out the tree.
std::const_pointer_cast<RootShadowNode>(nextRootNode)
->layoutIfNeeded(&affectedLayoutableNodes);
nextRootNode->sealRecursive();
allNodes.push_back(nextRootNode);
// Calculating mutations.
auto mutations =
calculateShadowViewMutations(*currentRootNode, *nextRootNode);
// Make sure that in a single frame, a DELETE for a
// view is not followed by a CREATE for the same view.
{
std::vector<int> deletedTags{};
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Delete) {
deletedTags.push_back(mutation.oldChildShadowView.tag);
}
}
for (const auto& mutation : mutations) {
if (mutation.type == ShadowViewMutation::Type::Create) {
if (std::find(
deletedTags.begin(),
deletedTags.end(),
mutation.newChildShadowView.tag) != deletedTags.end()) {
LOG(ERROR) << "Deleted tag was recreated in mutations list: ["
<< mutation.newChildShadowView.tag << "]";
react_native_assert(false);
}
}
}
}
// Mutating the view tree.
viewTree.mutate(mutations);
// Building a view tree to compare with.
auto rebuiltViewTree =
buildStubViewTreeWithoutUsingDifferentiator(*nextRootNode);
// Comparing the newly built tree with the updated one.
if (rebuiltViewTree != viewTree) {
// Something went wrong.
LOG(ERROR) << "Entropy seed: " << entropy.getSeed() << "\n";
// There are some issues getting `getDebugDescription` to compile
// under test on Android for now.
#if RN_DEBUG_STRING_CONVERTIBLE
LOG(ERROR) << "Shadow Tree before: \n"
<< currentRootNode->getDebugDescription();
LOG(ERROR) << "Shadow Tree after: \n"
<< nextRootNode->getDebugDescription();
LOG(ERROR) << "View Tree before: \n"
<< getDebugDescription(viewTree.getRootStubView(), {});
LOG(ERROR) << "View Tree after: \n"
<< getDebugDescription(
rebuiltViewTree.getRootStubView(), {});
LOG(ERROR) << "Mutations:" << "\n"
<< getDebugDescription(mutations, {});
#endif
react_native_assert(false);
}
currentRootNode = nextRootNode;
}
}
SUCCEED();
}
} // namespace facebook::react
using namespace facebook::react;
TEST(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
/* seed */ 0,
/* size */ 512,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener2) {
testShadowNodeTreeLifeCycle(
/* seed */ 1,
/* size */ 512,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
stableSmallerTreeMoreIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
/* seed */ 0,
/* size */ 16,
/* repeats */ 512,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableSmallerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 32,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableBiggerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 256,
/* repeats */ 32,
/* stages */ 32);
}
TEST(
ShadowTreeLifecycleTest,
unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
/* seed */ 1337,
/* size */ 32,
/* repeats */ 512,
/* stages */ 32);
}
// failing test case found 4-25-2021
// TODO: T213669056
// TEST(
// ShadowTreeLifecycleTest,
// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011)
// {
// testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
// /* seed */ 1167342011,
// /* size */ 32,
// /* repeats */ 512,
// /* stages */ 32);
// }
// You may uncomment this - locally only! - to generate failing seeds.
// TEST(
// ShadowTreeLifecycleTest,
// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflatteningManyRandom)
// {
// std::random_device device;
// for (int i = 0; i < 10; i++) {
// uint_fast32_t seed = device();
// LOG(ERROR) << "Seed: " << seed;
// testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
// /* seed */ seed,
// /* size */ 32,
// /* repeats */ 512,
// /* stages */ 32);
// }
// }

View File

@@ -0,0 +1,978 @@
/*
* 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 <memory>
#include <gtest/gtest.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/graphics/ValueUnit.h>
#include <react/renderer/mounting/Differentiator.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include <react/renderer/mounting/stubs/stubs.h>
namespace facebook::react {
class StackingContextTest : public ::testing::Test {
protected:
std::unique_ptr<ComponentBuilder> builder_;
std::shared_ptr<RootShadowNode> rootShadowNode_;
std::shared_ptr<ViewShadowNode> nodeA_;
std::shared_ptr<ViewShadowNode> nodeAA_;
std::shared_ptr<ViewShadowNode> nodeB_;
std::shared_ptr<ViewShadowNode> nodeBA_;
std::shared_ptr<ViewShadowNode> nodeBB_;
std::shared_ptr<ViewShadowNode> nodeBBA_;
std::shared_ptr<ViewShadowNode> nodeBBB_;
std::shared_ptr<ViewShadowNode> nodeBC_;
std::shared_ptr<ViewShadowNode> nodeBD_;
std::shared_ptr<RootShadowNode> currentRootShadowNode_;
StubViewTree currentStubViewTree_;
void SetUp() override {
// ┌────────────── (Root) ──────────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┃ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// └────────────────────────────────────┘
// clang-format off
auto element =
Element<RootShadowNode>()
.reference(rootShadowNode_)
.tag(1)
.children({
Element<ViewShadowNode>()
.tag(2)
.reference(nodeA_)
.children({
Element<ViewShadowNode>()
.tag(3)
.reference(nodeAA_)
}),
Element<ViewShadowNode>()
.tag(4)
.reference(nodeB_)
.children({
Element<ViewShadowNode>()
.tag(5)
.reference(nodeBA_),
Element<ViewShadowNode>()
.tag(6)
.reference(nodeBB_)
.children({
Element<ViewShadowNode>()
.tag(7)
.reference(nodeBBA_),
Element<ViewShadowNode>()
.tag(8)
.reference(nodeBBB_)
}),
Element<ViewShadowNode>()
.tag(9)
.reference(nodeBC_),
Element<ViewShadowNode>()
.tag(10)
.reference(nodeBD_)
})
});
// clang-format on
builder_ = std::make_unique<ComponentBuilder>(simpleComponentBuilder());
builder_->build(element);
currentRootShadowNode_ = rootShadowNode_;
currentRootShadowNode_->layoutIfNeeded();
currentStubViewTree_ =
buildStubViewTreeWithoutUsingDifferentiator(*currentRootShadowNode_);
}
void TearDown() override {
ReactNativeFeatureFlags::dangerouslyReset();
}
void mutateViewShadowNodeProps_(
const std::shared_ptr<ViewShadowNode>& node,
std::function<void(ViewProps& props)> callback) {
rootShadowNode_ =
std::static_pointer_cast<RootShadowNode>(rootShadowNode_->cloneTree(
node->getFamily(), [&](const ShadowNode& oldShadowNode) {
auto viewProps = std::make_shared<ViewShadowNodeProps>();
callback(*viewProps);
return oldShadowNode.clone(
ShadowNodeFragment{.props = viewProps});
}));
}
void testViewTree_(
const std::function<void(const StubViewTree& viewTree)>& callback) {
rootShadowNode_->layoutIfNeeded();
callback(buildStubViewTreeUsingDifferentiator(*rootShadowNode_));
callback(buildStubViewTreeWithoutUsingDifferentiator(*rootShadowNode_));
auto mutations =
calculateShadowViewMutations(*currentRootShadowNode_, *rootShadowNode_);
currentRootShadowNode_ = rootShadowNode_;
currentStubViewTree_.mutate(mutations);
callback(currentStubViewTree_);
}
};
TEST_F(StackingContextTest, defaultPropsMakeEverythingFlattened) {
testViewTree_([](const StubViewTree& viewTree) {
// 1 view in total.
EXPECT_EQ(viewTree.size(), 1);
// The root view has no subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 0);
});
}
TEST_F(StackingContextTest, mostPropsDoNotForceViewsToMaterialize) {
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ───────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ padding: 10; ┃ ┃ │ │ │
// │ ┃ ┃ margin: 9001; ┃ ┃ │ │ │
// │ ┃ ┃ position: absolute; ┃ ┃ │ │ │
// │ ┃ ┃ shadowRadius: 10; ┃ ┃ │ │ │
// │ ┃ ┃ shadowOffset: [42, 42]; ┃ ┃ │ │ │
// │ ┃ ┃ backgroundColor: clear; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ zIndex: 42; ┃ ┃ │ │ │
// │ ┃ ┃ margin: 42; ┃ ┃ │ │ │
// │ ┃ ┃ shadowColor: clear; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ No observable side-effects. │
// │ ┃ ┃ ┃ ┃ │━━━▶│ No views are generated. │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ borderRadii: 42; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ borderColor: black; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ onLayout: true; ┃ ┃ │ │ │
// │ ┃ ┃ hitSlop: 42; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPadding(yoga::Edge::All, yoga::StyleLength::points(42));
yogaStyle.setMargin(yoga::Edge::All, yoga::StyleLength::points(42));
yogaStyle.setPositionType(yoga::PositionType::Absolute);
props.shadowRadius = 42;
props.shadowOffset = Size{.width = 42, .height = 42};
props.backgroundColor = clearColor();
});
mutateViewShadowNodeProps_(nodeBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
props.zIndex = 42;
yogaStyle.setPositionType(yoga::PositionType::Static);
yogaStyle.setMargin(yoga::Edge::All, yoga::StyleLength::points(42));
props.shadowColor = clearColor();
props.shadowOpacity = 0.42;
});
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.borderRadii.all = ValueUnit{42, UnitType::Point};
props.borderColors.all = blackColor();
});
mutateViewShadowNodeProps_(nodeBD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
props.onLayout = true;
props.hitSlop = EdgeInsets{42, 42, 42, 42};
yogaStyle.setPositionType(yoga::PositionType::Static);
});
testViewTree_([](const StubViewTree& viewTree) {
// 1 view in total.
EXPECT_EQ(viewTree.size(), 1);
// The root view has no subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 0);
});
}
TEST_F(StackingContextTest, somePropsForceViewsToMaterialize1) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ ┃ backgroundColor: black; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ backgroundColor: white; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ shadowColor: black; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeAA_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(
nodeBA_, [](ViewProps& props) { props.backgroundColor = whiteColor(); });
mutateViewShadowNodeProps_(
nodeBBA_, [](ViewProps& props) { props.shadowColor = blackColor(); });
testViewTree_([](const StubViewTree& viewTree) {
// 4 views in total.
EXPECT_EQ(viewTree.size(), 4);
// The root view has all 3 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 3);
// The root view subviews are [3, 5, 7].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 3);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 7);
});
}
TEST_F(StackingContextTest, somePropsForceViewsToMaterialize2) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ A (tag: 2) ━━━━━━━━━━━━┓ │
// │ ┃ backgroundColor: black; ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┃ pointerEvents: none; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ B (tag: 4) ━━━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ testId: "42" ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BB (tag: 6) ━━━━━━━━━━━┓ │
// │ ┃ ┃ nativeId: "42" ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ backgroundColor: black; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBB (tag: 8) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ transform: scale(2); ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━━┓ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ zIndex: 42; ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┏━ BD (tag: 10) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ shadowColor: black; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ opacity: 0.42; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeA_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
props.pointerEvents = PointerEventsMode::None;
});
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.testId = "42"; });
mutateViewShadowNodeProps_(
nodeBA_, [](ViewProps& props) { props.nativeId = "42"; });
mutateViewShadowNodeProps_(
nodeBB_, [](ViewProps& props) { props.backgroundColor = blackColor(); });
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
props.transform = Transform::Scale(2, 2, 2);
});
mutateViewShadowNodeProps_(nodeBBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 42;
});
mutateViewShadowNodeProps_(
nodeBC_, [](ViewProps& props) { props.shadowColor = blackColor(); });
mutateViewShadowNodeProps_(
nodeBD_, [](ViewProps& props) { props.opacity = 0.42; });
testViewTree_([](const StubViewTree& viewTree) {
// 10 views in total.
EXPECT_EQ(viewTree.size(), 10);
// The root view has all 9 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 9);
});
}
TEST_F(StackingContextTest, nonCollapsableChildren) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BBB (tag: 8) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ |
// │ ┃ ┃ ┃ ┃ │ │ |
// │ ┃ ┃ ┃ ┃ │ │ |
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ |
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ |
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┃ │ │ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ collapsableChildren: false ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeBB_, [](ViewProps& props) { props.collapsableChildren = false; });
testViewTree_([](const StubViewTree& viewTree) {
// 3 views in total.
EXPECT_EQ(viewTree.size(), 3);
// The root view has all 2 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 2);
// The root view subviews are [7,8].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 7);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 8);
});
}
TEST_F(StackingContextTest, nonCollapsableChildrenMixed) {
// ┌────────────── (Root) ──────────────┐ ┌─────────── (Root) ──────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BA (tag: 5) ━━ ━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BB (tag: 6) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┏━ BBA (tag: 7) ━━━-━━┓ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃FormsView ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃FormsStackingContext ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┗━━━━━━━━━━━━━━━━━━━━-┛ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BD (tag: 10) ━━━━━━━━━━┓ │
// │ ┃ collapsableChildren: false ┃ │ │ ┃FormsView ┃ │
// │ ┃ ┃ │ │ ┃FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ testId: "42" ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ collapsable: true ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ testId: "123" ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(
nodeB_, [](ViewProps& props) { props.collapsableChildren = false; });
mutateViewShadowNodeProps_(
nodeBBA_, [](ViewProps& props) { props.testId = "42"; });
mutateViewShadowNodeProps_(
nodeBC_, [](ViewProps& props) { props.collapsable = true; });
mutateViewShadowNodeProps_(
nodeBD_, [](ViewProps& props) { props.testId = "43"; });
testViewTree_([](const StubViewTree& viewTree) {
// 6 views in total.
EXPECT_EQ(viewTree.size(), 6);
// The root view has four of the subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
// The root view subviews are [5, 6, 9, 10].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 6);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->children.size(), 1);
EXPECT_EQ(
viewTree.getRootStubView().children.at(1)->children.at(0)->tag, 7);
});
}
TEST_F(StackingContextTest, zIndexAndFlattenedNodes) {
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ───────────┐
// │ ┏━ A (tag: 2) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BD (tag: 10) ━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ AA (tag: 3) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━━┓ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ zIndex: 9001; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ ┏━ BBB (tag: 8) ━━━━━━━━━━┓ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┃ ┃ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┏━ B (tag: 4) ━━━━━━━━━━━━━━━━━━━┓ │ │ ┏━ BBA (tag: 7) ━━━━━━━━━━┓ │
// │ ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┏━ BA (tag: 5) ━━━━━━━━━━━━━━┓ ┃ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━━┓ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ zIndex: 9000; ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━━┓ │
// │ ┃ ┏━ BB (tag: 6) ━━━━━━━━━━━━━━┓ ┃ │ │ ┃ #FormsView ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┃ #FormsStackingContext ┃ │
// │ ┃ ┃ ┃ ┃ │━━━▶│ ┃ ┃ │
// │ ┃ ┃ ┃ ┃ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBA (tag: 7) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ zIndex: 8999; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┃ ┏━ BBB (tag: 8) ━━━━━━━━━┓ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ position: relative; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ zIndex: 8998; ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BC (tag: 9) ━━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ │
// │ ┃ ┃ zIndex: 8997; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┃ ┏━ BD (tag: 10) ━━━━━━━━━━━━━┓ ┃ │ │ │
// │ ┃ ┃ position: relative; ┃ ┃ │ │ │
// │ ┃ ┃ zIndex: 8996; ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┃ ┃ ┃ │ │ │
// │ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │ │ │
// │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │ │
// └────────────────────────────────────┘ └─────────────────────────────┘
mutateViewShadowNodeProps_(nodeAA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 9001;
});
mutateViewShadowNodeProps_(nodeBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 9000;
});
mutateViewShadowNodeProps_(nodeBBA_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8999;
});
mutateViewShadowNodeProps_(nodeBBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8998;
});
mutateViewShadowNodeProps_(nodeBC_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8997;
});
mutateViewShadowNodeProps_(nodeBD_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 8996;
});
testViewTree_([](const StubViewTree& viewTree) {
// 7 views in total.
EXPECT_EQ(viewTree.size(), 7);
// The root view has all 6 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 6);
// The root view subviews are [10, 9, 8, 7, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 8);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 7);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(5)->tag, 3);
});
// And now let's make BB to form a Stacking Context with small order-index.
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ──────────┐
// │ ┌─ A (tag: 2) ───────────────────┐ │ │ ┏━ BB (tag: 6) ━━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┃ ┏━ BBB (tag: 8) ━━━━━┓ ┃ │
// │ │ ┌─ AA (tag: 3) ──────────────┐ │ │ │ ┃ ┃ #View ┃ ┃ │
// │ │ │ position: relative; │ │ │ │ ┃ ┃ #StackingContext ┃ ┃ │
// │ │ │ zIndex: 9001; │ │ │ │ ┃ ┃ ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┗━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ │ │ │ │ │ │ ┃ ┏━ BBA (tag: 7) ━━━━━┓ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ #View ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ #StackingContext ┃ ┃ │
// │ │ │ │ │ │ │ ┃ ┃ ┃ ┃ │
// │ │ └────────────────────────────┘ │ │ │ ┃ ┗━━━━━━━━━━━━━━━━━━━━┛ ┃ │
// │ └────────────────────────────────┘ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┌─ B (tag: 4) ───────────────────┐ │ │ ┏━ BD (tag: 10) ━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ BA (tag: 5) ──────────────┐ │ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━┓ │
// │ │ │ position: relative; │ │ │ │ ┃ #View ┃ │
// │ │ │ zIndex: 9000; │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ └────────────────────────────┘ │ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━┓ │
// │ │ ╔═ BB (tag: 6) ══════════════╗ │ │ │ ┃ #View ┃ │
// │ │ ║ *** position: relative; ║ │ │ │ ┃ #StackingContext ┃ │
// │ │ ║ *** zIndex: 42; ║ │ │━━━━▶│ ┃ ┃ │
// │ │ ║ ║ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ║ ║ │ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━┓ │
// │ │ ║ ┌─ BBA (tag: 7) ─────────┐ ║ │ │ │ ┃ #View ┃ │
// │ │ ║ │ position: relative; │ ║ │ │ │ ┃ #StackingContext ┃ │
// │ │ ║ │ zIndex: 8999; │ ║ │ │ │ ┃ ┃ │
// │ │ ║ │ │ ║ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ║ ┌─ BBB (tag: 8) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8998; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ╚════════════════════════════╝ │ │ │ │
// │ │ ┌─ BC (tag: 9) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8997; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ┌─ BD (tag: 10) ─────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8996; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ └────────────────────────────────┘ │ │ │
// └────────────────────────────────────┘ └────────────────────────────┘
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Relative);
props.zIndex = 42;
});
testViewTree_([](const StubViewTree& viewTree) {
// 8 views in total.
EXPECT_EQ(viewTree.size(), 8);
// The root view has 5 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 5);
// The root view subviews are [6, 10, 9, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 6);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 3);
auto& view6 = viewTree.getStubView(6);
EXPECT_EQ(view6.children.size(), 2);
EXPECT_EQ(view6.children.at(0)->tag, 8);
EXPECT_EQ(view6.children.at(1)->tag, 7);
});
// And now, let's revert it back.
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setPositionType(yoga::PositionType::Static);
props.zIndex = {};
});
testViewTree_([](const StubViewTree& viewTree) {
// 7 views in total.
EXPECT_EQ(viewTree.size(), 7);
// The root view has all 6 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 6);
// The root view subviews are [10, 9, 8, 7, 5, 3].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 8);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 7);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(5)->tag, 3);
});
// And now, let's hide BB completety.
// ┌────────────── (Root) ──────────────┐ ┌────────── (Root) ──────────┐
// │ ┌─ A (tag: 2) ───────────────────┐ │ │ ┏━ BD (tag: 10) ━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ AA (tag: 3) ──────────────┐ │ │ │ ┏━ BC (tag: 9) ━━━━━━━━━━┓ │
// │ │ │ position: relative; │ │ │ │ ┃ #View ┃ │
// │ │ │ zIndex: 9001; │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ │ │ │ │ │ ┏━ BA (tag: 5) ━━━━━━━━━━┓ │
// │ │ │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ └────────────────────────────┘ │ │ │ ┃ ┃ │
// │ └────────────────────────────────┘ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ ┌─ B (tag: 4) ───────────────────┐ │ │ ┏━ AA (tag: 3) ━━━━━━━━━━┓ │
// │ │ │ │ │ ┃ #View ┃ │
// │ │ │ │ │ ┃ #StackingContext ┃ │
// │ │ │ │ │ ┃ ┃ │
// │ │ │ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ │
// │ │ ┌─ BA (tag: 5) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 9000; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ╔═ BB (tag: 6) ══════════════╗ │ │ │ │
// │ │ ║ *** display: none; ║ │ │ │ │
// │ │ ║ ║ │ │━━━━▶│ │
// │ │ ║ ║ │ │ │ │
// │ │ ║ ║ │ │ │ │
// │ │ ║ ┌─ BBA (tag: 7) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8999; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ║ ┌─ BBB (tag: 8) ─────────┐ ║ │ │ │ │
// │ │ ║ │ position: relative; │ ║ │ │ │ │
// │ │ ║ │ zIndex: 8998; │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ │ │ ║ │ │ │ │
// │ │ ║ └────────────────────────┘ ║ │ │ │ │
// │ │ ╚════════════════════════════╝ │ │ │ │
// │ │ ┌─ BC (tag: 9) ──────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8997; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ │ ┌─ BD (tag: 10) ─────────────┐ │ │ │ │
// │ │ │ position: relative; │ │ │ │ │
// │ │ │ zIndex: 8996; │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ └────────────────────────────┘ │ │ │ │
// │ └────────────────────────────────┘ │ │ │
// └────────────────────────────────────┘ └────────────────────────────┘
mutateViewShadowNodeProps_(nodeBB_, [](ViewProps& props) {
auto& yogaStyle = props.yogaStyle;
yogaStyle.setDisplay(yoga::Display::None);
});
testViewTree_([](const StubViewTree& viewTree) {
#ifdef ANDROID
// T153547836: Android still mounts views with
// ShadowNodeTraits::Trait::Hidden
EXPECT_EQ(viewTree.size(), 8);
// nodeBB_ forms a stacking context
EXPECT_EQ(viewTree.getRootStubView().children.size(), 5);
// The root view subviews are [6, 10, 9, 5].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 6);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(4)->tag, 3);
#else
EXPECT_EQ(viewTree.size(), 5);
// The root view has all 4 subviews.
EXPECT_EQ(viewTree.getRootStubView().children.size(), 4);
// The root view subviews are [6, 10, 9, 5].
EXPECT_EQ(viewTree.getRootStubView().children.at(0)->tag, 10);
EXPECT_EQ(viewTree.getRootStubView().children.at(1)->tag, 9);
EXPECT_EQ(viewTree.getRootStubView().children.at(2)->tag, 5);
EXPECT_EQ(viewTree.getRootStubView().children.at(3)->tag, 3);
#endif
});
}
} // namespace facebook::react

View File

@@ -0,0 +1,856 @@
/*
* 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 <memory>
#include <gtest/gtest.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/element/ComponentBuilder.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/mounting/MountingCoordinator.h>
#include <react/renderer/mounting/ShadowTree.h>
#include <react/renderer/mounting/ShadowTreeDelegate.h>
#include <react/renderer/element/testUtils.h>
using namespace facebook::react;
class DummyShadowTreeDelegate : public ShadowTreeDelegate {
public:
RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree& /*shadowTree*/,
const RootShadowNode::Shared& /*oldRootShadowNode*/,
const RootShadowNode::Unshared& newRootShadowNode,
const ShadowTree::CommitOptions& /*commitOptions*/) const override {
return newRootShadowNode;
}
void shadowTreeDidFinishTransaction(
std::shared_ptr<const MountingCoordinator> mountingCoordinator,
bool /*mountSynchronously*/) const override {}
};
namespace {
const ShadowNode* findDescendantNode(
const ShadowNode& shadowNode,
const ShadowNodeFamily& family) {
if (&shadowNode.getFamily() == &family) {
return &shadowNode;
}
for (const auto& childNode : shadowNode.getChildren()) {
auto descendant = findDescendantNode(*childNode, family);
if (descendant != nullptr) {
return descendant;
}
}
return nullptr;
}
const ShadowNode* findDescendantNode(
const ShadowTree& shadowTree,
const ShadowNodeFamily& family) {
return findDescendantNode(
*shadowTree.getCurrentRevision().rootShadowNode, family);
}
} // namespace
class StateReconciliationTest : public ::testing::TestWithParam<bool> {
public:
StateReconciliationTest() : builder_(simpleComponentBuilder()) {}
ComponentBuilder builder_;
};
TEST_F(StateReconciliationTest, testStateReconciliation) {
// ==== SETUP ====
/*
<Root>
<View>
<ScrollView />
</View>
</Root>
*/
auto parentShadowNode = std::shared_ptr<ViewShadowNode>{};
auto scrollViewInitialShadowNode = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ViewShadowNode>()
.reference(parentShadowNode).children({
Element<ScrollViewShadowNode>()
.reference(scrollViewInitialShadowNode)
})
});
// clang-format on
ContextContainer contextContainer{};
auto initialRootShadowNode = builder_.build(element);
auto rootShadowNodeState1 = initialRootShadowNode->ShadowNode::clone({});
auto& scrollViewComponentDescriptor =
scrollViewInitialShadowNode->getComponentDescriptor();
auto& scrollViewFamily = scrollViewInitialShadowNode->getFamily();
auto initialState = scrollViewInitialShadowNode->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== INITIAL COMMIT ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState1);
},
{.enableStateReconciliation = true});
EXPECT_EQ(initialState->getMostRecentState(), initialState);
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState1, scrollViewFamily)->getState(),
initialState);
// ==== COMMIT with new State 2 ====
auto state2 = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeState2 = initialRootShadowNode->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = state2});
});
EXPECT_EQ(
findDescendantNode(*initialRootShadowNode, scrollViewFamily)->getState(),
initialState);
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState2, scrollViewFamily)->getState(),
state2);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState2);
},
{.enableStateReconciliation = false});
EXPECT_EQ(initialState->getMostRecentState(), state2);
EXPECT_EQ(state2->getMostRecentState(), state2);
// ==== COMMIT with new State 3 ====
auto state3 = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeState3 = rootShadowNodeState2->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = state3});
});
EXPECT_EQ(
findDescendantNode(*rootShadowNodeState3, scrollViewFamily)->getState(),
state3);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState3);
},
{.enableStateReconciliation = false});
EXPECT_EQ(
findDescendantNode(shadowTree, scrollViewFamily)->getState(), state3);
EXPECT_EQ(initialState->getMostRecentState(), state3);
EXPECT_EQ(state2->getMostRecentState(), state3);
EXPECT_EQ(state3->getMostRecentState(), state3);
// ==== COMMIT from React ====
auto rootShadowNode = rootShadowNodeState2->cloneTree(
parentShadowNode->getFamily(),
[&](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({}); });
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode);
},
{.enableStateReconciliation = true});
EXPECT_EQ(
findDescendantNode(shadowTree, scrollViewFamily)
->getState()
->getRevision(),
state3->getRevision());
}
TEST_F(StateReconciliationTest, testCloneslessStateReconciliationDoesntClone) {
// ==== SETUP ====
/*
<Root>
<ScrollView />
</Root>
*/
auto initialScrollViewShadowNode = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ScrollViewShadowNode>()
.reference(initialScrollViewShadowNode)
});
// clang-format on
ContextContainer contextContainer{};
auto rootShadowNode1 = builder_.build(element);
auto& scrollViewComponentDescriptor =
initialScrollViewShadowNode->getComponentDescriptor();
auto& scrollViewFamily = initialScrollViewShadowNode->getFamily();
auto initialState = initialScrollViewShadowNode->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== Initial commit ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode1);
},
{.enableStateReconciliation = true});
EXPECT_EQ(initialState->getMostRecentState(), initialState);
EXPECT_EQ(
findDescendantNode(*rootShadowNode1, scrollViewFamily)->getState(),
initialState);
// ==== C++ state update commit ====
auto state2 = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNode2 = rootShadowNode1->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = state2});
});
EXPECT_EQ(
findDescendantNode(*rootShadowNode2, scrollViewFamily)->getState(),
state2);
EXPECT_EQ(initialState->getMostRecentState(), initialState);
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode2);
},
{.enableStateReconciliation = false});
EXPECT_EQ(initialState->getMostRecentState(), state2);
EXPECT_EQ(state2->getMostRecentState(), state2);
// ==== Creact clones tree ====
std::shared_ptr<ShadowNode> newlyClonedShadowNode;
auto rootShadowNodeClonedFromReact = rootShadowNode2->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
newlyClonedShadowNode = oldShadowNode.clone({});
return newlyClonedShadowNode;
});
auto state3 = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate = rootShadowNode2->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = state3});
});
// ==== State update ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{.enableStateReconciliation = false});
// ==== React commit ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
auto scrollViewShadowNode = findDescendantNode(shadowTree, scrollViewFamily);
EXPECT_EQ(scrollViewShadowNode->getState(), state3);
}
TEST_F(StateReconciliationTest, testStateReconciliationScrollViewChildUpdate) {
// ==== SETUP ====
/*
<Root>
<ScrollView>
<View />
</ScrollView>
</Root>
*/
auto initialScrollViewShadowNode = std::shared_ptr<ScrollViewShadowNode>{};
auto initialChildViewShadowNode = std::shared_ptr<ViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ScrollViewShadowNode>()
.reference(initialScrollViewShadowNode)
.children({
Element<ViewShadowNode>()
.reference(initialChildViewShadowNode)
})
});
// clang-format on
ContextContainer contextContainer{};
auto initialRootShadowNode = builder_.build(element);
auto& scrollViewComponentDescriptor =
initialScrollViewShadowNode->getComponentDescriptor();
auto& scrollViewFamily = initialScrollViewShadowNode->getFamily();
auto initialState = initialScrollViewShadowNode->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== Initial commit ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(initialRootShadowNode);
},
{.enableStateReconciliation = true});
// ==== React starts cloning but does not commit ====
std::shared_ptr<ShadowNode> newlyClonedViewShadowNode;
auto rootShadowNodeClonedFromReact = initialRootShadowNode->cloneTree(
initialChildViewShadowNode->getFamily(),
[&](const ShadowNode& oldShadowNode) {
auto& viewComponentDescriptor =
initialChildViewShadowNode->getComponentDescriptor();
PropsParserContext parserContext{-1, contextContainer};
auto props =
viewComponentDescriptor.cloneProps(parserContext, nullptr, {});
newlyClonedViewShadowNode = oldShadowNode.clone({});
return newlyClonedViewShadowNode;
});
// ==== State update ====
auto state2 = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNode2 = initialRootShadowNode->cloneTree(
scrollViewFamily, [&](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = state2});
});
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootShadowNode2);
},
{.enableStateReconciliation = false});
// ==== React commits its tree ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
auto scrollViewShadowNode = findDescendantNode(shadowTree, scrollViewFamily);
EXPECT_EQ(scrollViewShadowNode->getState(), state2);
EXPECT_EQ(
findDescendantNode(shadowTree, initialChildViewShadowNode->getFamily()),
newlyClonedViewShadowNode.get());
}
TEST_F(StateReconciliationTest, testScrollViewWithChildrenDeletion) {
// ==== SETUP ====
/*
<Root>
<View> - parent
<ScrollView /> - child A - will be deleted.
<ScrollView /> - child B - will remain and its props are updated.
</View>
</Root>
*/
auto parentView = std::shared_ptr<ViewShadowNode>{};
auto childA = std::shared_ptr<ScrollViewShadowNode>{};
auto childB = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ViewShadowNode>()
.reference(parentView)
.children({
Element<ScrollViewShadowNode>()
.reference(childA),
Element<ScrollViewShadowNode>()
.reference(childB),
})
});
// clang-format on
ContextContainer contextContainer{};
auto rootNode = builder_.build(element);
auto& scrollViewComponentDescriptor = childB->getComponentDescriptor();
auto& childBFamily = childB->getFamily();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== INITIAL COMMIT ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{.enableStateReconciliation = true});
// ==== Tree without childA and childB has new props ====
auto rootShadowNodeClonedFromReact = rootNode->cloneTree(
parentView->getFamily(),
[&childB, &scrollViewComponentDescriptor, &contextContainer](
const ShadowNode& oldShadowNode) {
PropsParserContext parserContext{-1, contextContainer};
auto clonedChildB = childB->clone({
.props = scrollViewComponentDescriptor.cloneProps(
parserContext, nullptr, {}),
});
std::shared_ptr<const ShadowNode> shadowNode = clonedChildB;
std::vector<std::shared_ptr<const ShadowNode>> children =
std::vector({shadowNode});
const auto childrenShared =
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
children);
return oldShadowNode.clone({.children = childrenShared});
});
// ==== State update ====
auto newState = scrollViewComponentDescriptor.createState(
childBFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree(
childBFamily, [&newState](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = newState});
});
shadowTree.commit(
[&rootShadowNodeClonedFromStateUpdate](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{.enableStateReconciliation = false});
EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr);
// ==== Now the react commit happens. ====
shadowTree.commit(
[&rootShadowNodeClonedFromReact](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
EXPECT_EQ(findDescendantNode(shadowTree, childA->getFamily()), nullptr);
EXPECT_EQ(
findDescendantNode(shadowTree, childB->getFamily())->getState(),
newState);
}
TEST_F(StateReconciliationTest, testScrollViewWithComplexChildrenReorder) {
// ==== SETUP ====
/*
<Root>
<View> - grandparent
<View> - parent A
<ScrollView /> - child A
</View>
<View> - parent B
<ScrollView /> - child B
</View>
</View>
</Root>
*/
auto grandParent = std::shared_ptr<ViewShadowNode>{};
auto childA = std::shared_ptr<ScrollViewShadowNode>{};
auto childB = std::shared_ptr<ScrollViewShadowNode>{};
auto parentA = std::shared_ptr<ViewShadowNode>{};
auto parentB = std::shared_ptr<ViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ViewShadowNode>()
.reference(grandParent)
.children({
Element<ViewShadowNode>()
.reference(parentA)
.children({
Element<ScrollViewShadowNode>()
.reference(childA)
}),
Element<ViewShadowNode>()
.reference(parentB)
.children({
Element<ScrollViewShadowNode>()
.reference(childB)
}),
})
});
// clang-format on
ContextContainer contextContainer{};
auto rootNode = builder_.build(element);
auto& scrollViewComponentDescriptor = childB->getComponentDescriptor();
auto& childAFamily = childA->getFamily();
auto initialState = childA->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== INITIAL COMMIT ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{.enableStateReconciliation = true});
// ==== Tree swapping childA and childB. ChildB has new props ====
auto rootShadowNodeClonedFromReact = rootNode->cloneTree(
grandParent->getFamily(),
[&parentA, &parentB](const ShadowNode& oldShadowNode) {
auto children =
std::vector<std::shared_ptr<const ShadowNode>>({parentB, parentA});
const auto childrenShared =
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
children);
return oldShadowNode.clone({.children = childrenShared});
});
// ==== State update ====
auto newState = scrollViewComponentDescriptor.createState(
childAFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree(
childAFamily, [&newState](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = newState});
});
shadowTree.commit(
[&rootShadowNodeClonedFromStateUpdate](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{.enableStateReconciliation = false});
// ==== Now the react commit happens. ====
shadowTree.commit(
[&rootShadowNodeClonedFromReact](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr);
EXPECT_NE(findDescendantNode(shadowTree, childB->getFamily()), nullptr);
EXPECT_EQ(findDescendantNode(shadowTree, childAFamily)->getState(), newState);
}
TEST_F(StateReconciliationTest, testScrollViewWithChildrenReorder) {
// ==== SETUP ====
/*
<Root>
<View> - parent
<ScrollView /> - child A - will be moved to 2nd position.
<ScrollView /> - child B - will will be moved to 1st position.
</View>
</Root>
*/
auto parentView = std::shared_ptr<ViewShadowNode>{};
auto childA = std::shared_ptr<ScrollViewShadowNode>{};
auto childB = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ViewShadowNode>()
.reference(parentView)
.children({
Element<ScrollViewShadowNode>()
.reference(childA),
Element<ScrollViewShadowNode>()
.reference(childB),
})
});
// clang-format on
ContextContainer contextContainer{};
auto rootNode = builder_.build(element);
auto& scrollViewComponentDescriptor = childB->getComponentDescriptor();
auto& childAFamily = childA->getFamily();
auto initialState = childA->getState();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{11},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== INITIAL COMMIT ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{.enableStateReconciliation = true});
// ==== Tree swapping childA and childB. ChildB has new props ====
auto rootShadowNodeClonedFromReact = rootNode->cloneTree(
parentView->getFamily(),
[&childB, &childA](const ShadowNode& oldShadowNode) {
auto children =
std::vector<std::shared_ptr<const ShadowNode>>({childB, childA});
const auto childrenShared =
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
children);
return oldShadowNode.clone({.children = childrenShared});
});
// ==== State update ====
auto newState = scrollViewComponentDescriptor.createState(
childAFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree(
childAFamily, [&newState](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = newState});
});
shadowTree.commit(
[&rootShadowNodeClonedFromStateUpdate](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{.enableStateReconciliation = false});
// ==== Now the react commit happens. ====
shadowTree.commit(
[&rootShadowNodeClonedFromReact](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr);
EXPECT_NE(findDescendantNode(shadowTree, childB->getFamily()), nullptr);
EXPECT_EQ(findDescendantNode(shadowTree, childAFamily)->getState(), newState);
}
TEST_F(StateReconciliationTest, testScrollViewWithChildrenAddition) {
// ==== SETUP ====
/*
<Root>
<View> - parent
<ScrollView /> - child A - will be added.
<ScrollView /> - child B - will stay
</View>
</Root>
*/
auto parentView = std::shared_ptr<ViewShadowNode>{};
auto childA = std::shared_ptr<const ShadowNode>{};
auto childB = std::shared_ptr<ScrollViewShadowNode>{};
// clang-format off
auto element =
Element<RootShadowNode>()
.children({
Element<ViewShadowNode>()
.reference(parentView)
.children({
Element<ScrollViewShadowNode>()
.reference(childB),
})
});
// clang-format on
ContextContainer contextContainer{};
auto rootNode = builder_.build(element);
auto& scrollViewComponentDescriptor = childB->getComponentDescriptor();
auto& scrollViewFamily = childB->getFamily();
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
ShadowTree shadowTree{
SurfaceId{1},
LayoutConstraints{},
LayoutContext{},
shadowTreeDelegate,
contextContainer};
// ==== INITIAL COMMIT ====
shadowTree.commit(
[&](const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{.enableStateReconciliation = true});
// ==== State update ====
auto newState = scrollViewComponentDescriptor.createState(
scrollViewFamily, std::make_shared<const ScrollViewState>());
auto rootShadowNodeClonedFromStateUpdate = rootNode->cloneTree(
scrollViewFamily, [&newState](const ShadowNode& oldShadowNode) {
return oldShadowNode.clone({.state = newState});
});
// ==== Tree with new child ====
auto rootShadowNodeClonedFromReact = rootNode->cloneTree(
parentView->getFamily(),
[&childA, &childB, &contextContainer](const ShadowNode& oldShadowNode) {
auto& viewComponentDescriptor = childB->getComponentDescriptor();
auto childAFamily = viewComponentDescriptor.createFamily(
{.tag = 13, .surfaceId = 1, .instanceHandle = nullptr});
PropsParserContext parserContext{-1, contextContainer};
auto props =
viewComponentDescriptor.cloneProps(parserContext, nullptr, {});
childA = viewComponentDescriptor.createShadowNode(
{.props =
viewComponentDescriptor.cloneProps(parserContext, nullptr, {}),
.state = viewComponentDescriptor.createInitialState(
props, childAFamily)},
childAFamily);
std::shared_ptr<const ShadowNode> shadowNode = childA;
auto children = std::vector<std::shared_ptr<const ShadowNode>>(
{shadowNode, childB});
const auto childrenShared =
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
children);
return oldShadowNode.clone({.children = childrenShared});
});
// ==== State update happens ====
shadowTree.commit(
[&rootShadowNodeClonedFromStateUpdate](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromStateUpdate);
},
{.enableStateReconciliation = false});
// ==== React commits tree ====
shadowTree.commit(
[&rootShadowNodeClonedFromReact](
const RootShadowNode& /*oldRootShadowNode*/) {
return std::static_pointer_cast<RootShadowNode>(
rootShadowNodeClonedFromReact);
},
{.enableStateReconciliation = true});
EXPECT_NE(findDescendantNode(shadowTree, childA->getFamily()), nullptr);
EXPECT_EQ(
findDescendantNode(shadowTree, childB->getFamily())->getState(),
newState);
}

View File

@@ -0,0 +1,84 @@
/*
* 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 "updateMountedFlag.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
namespace facebook::react {
void updateMountedFlag(
const std::vector<std::shared_ptr<const ShadowNode>>& oldChildren,
const std::vector<std::shared_ptr<const ShadowNode>>& newChildren,
ShadowTreeCommitSource commitSource) {
// This is a simplified version of Diffing algorithm that only updates
// `mounted` flag on `ShadowNode`s. The algorithm sets "mounted" flag before
// "unmounted" to allow `ShadowNode` detect a situation where the node was
// remounted.
if (&oldChildren == &newChildren) {
// Lists are identical, nothing to do.
return;
}
if (oldChildren.empty() && newChildren.empty()) {
// Both lists are empty, nothing to do.
return;
}
size_t index = 0;
// Stage 1: Mount and unmount "updated" children.
for (index = 0; index < oldChildren.size() && index < newChildren.size();
index++) {
const auto& oldChild = oldChildren[index];
const auto& newChild = newChildren[index];
if (oldChild == newChild) {
// Nodes are identical, skipping the subtree.
continue;
}
if (!ShadowNode::sameFamily(*oldChild, *newChild)) {
// Totally different nodes, updating is impossible.
break;
}
newChild->setMounted(true);
oldChild->setMounted(false);
if (commitSource == ShadowTreeCommitSource::React &&
ReactNativeFeatureFlags::updateRuntimeShadowNodeReferencesOnCommit()) {
newChild->updateRuntimeShadowNodeReference(newChild);
}
updateMountedFlag(
oldChild->getChildren(), newChild->getChildren(), commitSource);
}
size_t lastIndexAfterFirstStage = index;
// State 2: Mount new children.
for (index = lastIndexAfterFirstStage; index < newChildren.size(); index++) {
const auto& newChild = newChildren[index];
newChild->setMounted(true);
if (commitSource == ShadowTreeCommitSource::React &&
ReactNativeFeatureFlags::updateRuntimeShadowNodeReferencesOnCommit()) {
newChild->updateRuntimeShadowNodeReference(newChild);
}
updateMountedFlag({}, newChild->getChildren(), commitSource);
}
// State 3: Unmount old children.
for (index = lastIndexAfterFirstStage; index < oldChildren.size(); index++) {
const auto& oldChild = oldChildren[index];
oldChild->setMounted(false);
updateMountedFlag(oldChild->getChildren(), {}, commitSource);
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
#pragma once
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/ShadowTree.h>
namespace facebook::react {
/*
* Traverses the shadow tree and updates the `mounted` flag on all nodes.
*/
void updateMountedFlag(
const std::vector<std::shared_ptr<const ShadowNode>> &oldChildren,
const std::vector<std::shared_ptr<const ShadowNode>> &newChildren,
ShadowTreeCommitSource commitSource);
} // namespace facebook::react