mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: # Summary In previous diffs earlier in 2020, we made changes to detect and optimize reordering of views when the order of views changed underneath the same parent. However, until now we have ignored reparenting and there's evidence of issues because of that. Because Fabric flattens views more aggressively, reparenting is also marginally more likely to happen. This diff introduces a very general Reparenting detection. It will work with view flattening/unflattening, as well as tree grafting - subtrees moved to entirely different parts of the tree, not just a single parent disappearing or reappearing because of flattening/unflattening. There is also another consideration: previously, we were generating strictly too many Create+Delete operations that were redundant and could cause consistency issues, crashes, or bugs on platforms that do not handle that gracefully - especially since the ordering of the Create+Delete is not guaranteed (a reparented view could be created "first" and then the differ could later issue a "delete" for the same view). Intuition behind how it works: we know the cases where we can detect reparenting: it's when nodes are *not* matched up with another node from the other tree, and we're either trying to delete an entire subtree, or create an entire subtree. For perf reasons, we generate whatever set of operations comes first (say, we generate all the Delete and Remove instructions) and take note in the `ReparentingMetadata` data-structure that Delete and/or Remove have been performed for each tag (if ordering is different, we do the same for Create+Insert if those come first). Then if we later detect a corresponding subtree creation/deletion, we don't generate those mutations and we mark the previous mutations for deletion. This incurs some map lookup cost, but this is only wasteful for commits where a large tree is deleted and a large tree is created, without reparenting. We may be able to improve perf further for certain edge-cases in the future. # Why can't we solve this in JS? Two things: 1. We certainly can avoid reparenting situations in JS, but it's trickier than before because of Fabric's view flattening logic - product engineers would have to think much harder about how to prevent reparenting in the general case. 2. In the case of specific views like BottomSheet that may crash if they're reparented, the solution is to make sure that the BottomSheet and the first child of the BottomSheet is never memoized, so that lifecycle functions and render are called more often; and that in every render, the BottomSheet manually clones its child, so that when the Views are recreated, the child of the BottomSheet has a tag and is an entirely different instance. This is certainly possible to do but feels like an onerous requirement for product teams, and it could be challenging to track down every specific BottomSheet that is memoized and/or hoist them higher in the view hierarchy so they're not reparented as often. Reviewed By: shergin Differential Revision: D23123575 fbshipit-source-id: 2fa7e1f026f87b6f0c60cad469a3ba85cdc234de
125 lines
4.2 KiB
C++
125 lines
4.2 KiB
C++
/*
|
|
* Copyright (c) Facebook, Inc. and its 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 <better/optional.h>
|
|
#include <chrono>
|
|
|
|
#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>
|
|
#include "ShadowTreeRevision.h"
|
|
|
|
#ifdef RN_SHADOW_TREE_INTROSPECTION
|
|
#include <react/renderer/mounting/stubs.h>
|
|
#endif
|
|
|
|
namespace facebook {
|
|
namespace 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:
|
|
using Shared = std::shared_ptr<MountingCoordinator const>;
|
|
|
|
/*
|
|
* The constructor is meant to be used only inside `ShadowTree`, and it's
|
|
* `public` only to enable using with `std::make_shared<>`.
|
|
*/
|
|
MountingCoordinator(
|
|
ShadowTreeRevision baseRevision,
|
|
std::weak_ptr<MountingOverrideDelegate const> delegate,
|
|
bool enableReparentingDetection = false);
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
better::optional<MountingTransaction> pullTransaction() 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;
|
|
|
|
TelemetryController const &getTelemetryController() const;
|
|
|
|
/*
|
|
* Methods from this section are meant to be used by
|
|
* `MountingOverrideDelegate` only.
|
|
*/
|
|
public:
|
|
void updateBaseRevision(ShadowTreeRevision const &baseRevision) const;
|
|
void resetLatestRevision() 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:
|
|
SurfaceId const surfaceId_;
|
|
|
|
mutable std::mutex mutex_;
|
|
mutable ShadowTreeRevision baseRevision_;
|
|
mutable better::optional<ShadowTreeRevision> lastRevision_{};
|
|
mutable MountingTransaction::Number number_{0};
|
|
mutable std::condition_variable signal_;
|
|
std::weak_ptr<MountingOverrideDelegate const> mountingOverrideDelegate_;
|
|
|
|
TelemetryController telemetryController_;
|
|
|
|
bool enableReparentingDetection_{false}; // temporary
|
|
|
|
#ifdef RN_SHADOW_TREE_INTROSPECTION
|
|
void validateTransactionAgainstStubViewTree(
|
|
ShadowViewMutationList const &mutations,
|
|
bool assertEquality) const;
|
|
mutable StubViewTree stubViewTree_; // Protected by `mutex_`.
|
|
#endif
|
|
};
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|