mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
47280de85e
Summary:
Perf numbers for this stack are given in terms of before-stack and after-stack, but the changes are split up for ease of review, and also to show that this migration CAN happen per-component and is 100% opt-in. Most existing C++ components do not /need/ to change at all.
# Problem Statement
During certain renders (select critical scenarios in specific products), UIManagerBinding::createNode time takes over 50% of JS thread CPU time. This could be higher or lower depending on the specific product and interaction, but overall createNode takes a lot of CPU time. The question is: can we improve this? What is the minimal overhead needed?
The vast, vast majority of time is taken up by prop parsing (specifically, converting JS values across the JSI into concrete values on the C++ props structs). Other methods like appendChild, etc, do not take up a significant amount of time; so we conclude that createNode is special, and the JSI itself, or calling into C++, is not the problem. Props parsing is the perf problem.
Can we improve it? (Spoiler: yes)
# How does props parsing work today?
Today, props parsing works as follows:
1. The ConcreteComponentDescriptor will construct a RawPropsParser (one per component /type/, per application: so one for View, one for Image, one for Text... etc)
2. Once per component type per application, ConcreteComponentDescriptor will call "prepare" on the RawPropsParser with an empty, default-constructed ConcreteProps struct. This ConcreteProps struct will cause RawProps.at(field) for every single field.
3. Based on the RawProps::at calls in part 2, RawPropsParser constructs a Map from props string names (width, height, position, etc) to a position within a "value index" array.
4. The above is what happens before any actual props are parsed; and the RawPropsParser is now ready to parse actual Props.
5. When props are actually being parsed from a JSI dictionary, we now have two phases:
1. The RawPropsParser `preparse`s the RawProps, by iterating over the JSI map and filling in two additional data structures: a linear list of RawValues, and a mapping from the ValueIndex array (`keyIndexToValueIndex_`; see step 3) to a value's position in the values list (`value_` in RawPropsParser/RawProps);
2. The ConcretePropT constructor is called, which is the same as in step 2/3, which calls `fieldValue = rawProps.at("fieldName")` repeatedly.
3. For each `at` call, the RawProps will look up a prop name in the Map constructed in step 3, and either return an empty value, or map the key name to the `keyIndexToValueIndex_` array, which maps to a value in `values_`, which is then returned and further parsed.
So, a few things that become clear with the current architecture:
1. Complexity is a property of the number of /possible/ props that /can/ be parsed, not what is actually used in product code. This violates the "only pay for what you use" principal. If you have `<View opacity={0.5} />`, the ViewProps constructor will request ~170 properties, not 1!
2. There's a lot of pre-parsing which isn't free
3. The levels of indirection aren't free, and make cache misses more likely and pipelining is more challenging
4. The levels of indirection also require more memory - minor, but not free
# How can we improve it?
The goal is to improve props parsing with minimal or zero impact on backwards-compability. We should be able to migrate over components when it's clear there's a performance issue, without requiring everything gets migrated over at once. This both (1) helps us prove out the new architecture, (2) derisks the project, (3) gives us time, internally and externally, to perfect the APIs and gradually migrate everything over before deleting the old infrastructure code entirely.
Thus, the goal is to do something that introduces a zero-cost abstraction. This isn't entirely possible in practice, and in fact this method slightly regresses components that do not use the new architecture /at all/, while dramatically improving migrated components and causing the impact of the /old/ architecture to be minimal.
# Solution
1. We still keep the existing system in place entirely.
2. After Props are constructed (see ConcreteComponentDescriptor changes) we iterate over all the /values/ set from JS, and call PropsT::setProp. Incidentally, this allows us to easily reuse the same prop for multiple values for "free", which was expensive in the old system.
3. It's worth noting that this makes a Props struct "less immutable" than it was before, and essentially now we have a "builder pattern" for Props. (If we really wanted to, we could still require a single constructor for Props, and then actually use an intermediate PropsBuilder to accumulate values - but I don't think this overhead would be worth for the conceptual "immutability" win, and instead a "Construct/Set/Seal" model works fine, and we still have all the same guarantees of immutability after the parsing phase)
# Implementation Details
# How to properly construct a single Prop value
Minor detail: parsing a single prop is a 3-step process. We imagine two scenarios: (1) Creating a new ShadowNode/Props A from nothing/void, so the previous Props value is just the default constructor. (2) Cloning a ShadowNode A->B and therefore Props A must be copied to Props B before parsing.
We will denote this as a clone from A->B, where A may or may not be a previous node or a default-constructed Props node; and imagine in particular that we're setting the "opacity" value for PropsB.
We must first (1) copy a value over from the previous version of the Props struct, so B.opacity = A.opacity; (2) Determine if opacity has been set from JS. If so, and there is a value, B.opacity = parse(JSValue). (3) If JS has passed in a value for the prop, BUT the value is `null`, it means that JS is resetting or deleting the prop, so we must set it BACK to the default. In this case we set PropsB.opacity = DefaultConstructedProps.opacity.
We must take care in general to ensure that the correct behavior is achieved here, which should help to explain some of the code below.
## String comparisons vs hash comparisons
In the previous system, a RawPropsKey is three `const char*` strings, concatenated together repeatedly /at runtime/. In practice, the ONLY reason we have the prefix, name, suffix Key structure is for the templated prop parsing in ViewProps and YogaStyableProps - that's it. It's not used anywhere else. Further, the key {"margin", "Left", "Width"} is identical to the key {"marginLeftWidth", null, null} and we don't do anything fancy with matching prefixes before comparing the whole string, or similar. Before comparison, keys are concatenated into a single string and then we use `strcmp`. The performance of this isn't terrible, but it's nonzero overhead.
I think we can do better and it's sufficient to compare hashed string values; even better, we can construct most of these /at compile time/ using constexpr, and using `switch` statements guarantee no hash collisions within a single Props struct (it's possible there's a collision between Props.cpp and ViewProps.cpp, for example, since they're different switch statements). We may eventually want to be more robust against has collisions; I personally don't find the risk to be too great, hash collisions with these keys are exceedingly unlikely (or maybe I just like to live dangerously). Thus, at runtime, each setProp requires computing a single hash for the value coming from JS, and then int comparisons with a bunch of pre-compiled values.
If we want to be really paranoid, we could be robust to hash collisions by doing `switch COMPILED_HASH("opacity"): if (strcmp(strFromJs, "opacity") == 0)`. I'm happy to do this if there's enough concern.
## Macros
Yuck! I'm using lots of C preprocessor macros. In general I found this way, way easier in reducing code and (essentially) doing codegen for me vs templated code for the switch cases and hashing prop names at compile-time. Maybe there's a better way.
Changelog: [Added][Fabric] New API for efficient props construction
Reviewed By: javache
Differential Revision: D37050215
fbshipit-source-id: d2dcd351a93b9715cfeb5197eb0d6f9194ec6eb9
217 lines
7.3 KiB
C++
217 lines
7.3 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <functional>
|
|
#include <memory>
|
|
|
|
#include <react/debug/react_native_assert.h>
|
|
#include <react/renderer/components/view/ViewPropsInterpolation.h>
|
|
#include <react/renderer/core/ComponentDescriptor.h>
|
|
#include <react/renderer/core/EventDispatcher.h>
|
|
#include <react/renderer/core/Props.h>
|
|
#include <react/renderer/core/PropsParserContext.h>
|
|
#include <react/renderer/core/ShadowNode.h>
|
|
#include <react/renderer/core/ShadowNodeFragment.h>
|
|
#include <react/renderer/core/State.h>
|
|
#include <react/renderer/graphics/Float.h>
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
/*
|
|
* Default template-based implementation of ComponentDescriptor.
|
|
* Use your `ShadowNode` type as a template argument and override any methods
|
|
* if necessary.
|
|
*/
|
|
template <typename ShadowNodeT>
|
|
class ConcreteComponentDescriptor : public ComponentDescriptor {
|
|
static_assert(
|
|
std::is_base_of<ShadowNode, ShadowNodeT>::value,
|
|
"ShadowNodeT must be a descendant of ShadowNode");
|
|
|
|
using SharedShadowNodeT = std::shared_ptr<const ShadowNodeT>;
|
|
|
|
public:
|
|
using ConcreteShadowNode = ShadowNodeT;
|
|
using ConcreteProps = typename ShadowNodeT::ConcreteProps;
|
|
using SharedConcreteProps = typename ShadowNodeT::SharedConcreteProps;
|
|
using ConcreteEventEmitter = typename ShadowNodeT::ConcreteEventEmitter;
|
|
using SharedConcreteEventEmitter =
|
|
typename ShadowNodeT::SharedConcreteEventEmitter;
|
|
using ConcreteState = typename ShadowNodeT::ConcreteState;
|
|
using ConcreteStateData = typename ShadowNodeT::ConcreteState::Data;
|
|
|
|
ConcreteComponentDescriptor(ComponentDescriptorParameters const ¶meters)
|
|
: ComponentDescriptor(parameters) {
|
|
rawPropsParser_.prepare<ConcreteProps>();
|
|
}
|
|
|
|
ComponentHandle getComponentHandle() const override {
|
|
return ShadowNodeT::Handle();
|
|
}
|
|
|
|
ComponentName getComponentName() const override {
|
|
return ShadowNodeT::Name();
|
|
}
|
|
|
|
ShadowNodeTraits getTraits() const override {
|
|
return ShadowNodeT::BaseTraits();
|
|
}
|
|
|
|
ShadowNode::Shared createShadowNode(
|
|
const ShadowNodeFragment &fragment,
|
|
ShadowNodeFamily::Shared const &family) const override {
|
|
auto shadowNode =
|
|
std::make_shared<ShadowNodeT>(fragment, family, getTraits());
|
|
|
|
adopt(shadowNode);
|
|
|
|
return shadowNode;
|
|
}
|
|
|
|
ShadowNode::Unshared cloneShadowNode(
|
|
const ShadowNode &sourceShadowNode,
|
|
const ShadowNodeFragment &fragment) const override {
|
|
auto shadowNode = std::make_shared<ShadowNodeT>(sourceShadowNode, fragment);
|
|
|
|
adopt(shadowNode);
|
|
return shadowNode;
|
|
}
|
|
|
|
void appendChild(
|
|
const ShadowNode::Shared &parentShadowNode,
|
|
const ShadowNode::Shared &childShadowNode) const override {
|
|
auto concreteParentShadowNode =
|
|
std::static_pointer_cast<const ShadowNodeT>(parentShadowNode);
|
|
auto concreteNonConstParentShadowNode =
|
|
std::const_pointer_cast<ShadowNodeT>(concreteParentShadowNode);
|
|
concreteNonConstParentShadowNode->appendChild(childShadowNode);
|
|
}
|
|
|
|
virtual SharedProps cloneProps(
|
|
const PropsParserContext &context,
|
|
const SharedProps &props,
|
|
const RawProps &rawProps) const override {
|
|
// Optimization:
|
|
// Quite often nodes are constructed with default/empty props: the base
|
|
// `props` object is `null` (there no base because it's not cloning) and the
|
|
// `rawProps` is empty. In this case, we can return the default props object
|
|
// of a concrete type entirely bypassing parsing.
|
|
if (!props && rawProps.isEmpty()) {
|
|
return ShadowNodeT::defaultSharedProps();
|
|
}
|
|
|
|
rawProps.parse(rawPropsParser_, context);
|
|
|
|
// Call old-style constructor
|
|
auto shadowNodeProps = ShadowNodeT::Props(context, rawProps, props);
|
|
|
|
// Use the new-style iterator
|
|
// Note that we just check if `Props` has this flag set, no matter
|
|
// the type of ShadowNode; it acts as the single global flag.
|
|
if (Props::enablePropIteratorSetter) {
|
|
rawProps.iterateOverValues([&](RawPropsPropNameHash hash,
|
|
const char *propName,
|
|
RawValue const &fn) {
|
|
shadowNodeProps.get()->setProp(context, hash, propName, fn);
|
|
});
|
|
}
|
|
|
|
return shadowNodeProps;
|
|
};
|
|
|
|
SharedProps interpolateProps(
|
|
const PropsParserContext &context,
|
|
Float animationProgress,
|
|
const SharedProps &props,
|
|
const SharedProps &newProps) const override {
|
|
#ifdef ANDROID
|
|
// On Android only, the merged props should have the same RawProps as the
|
|
// final props struct
|
|
SharedProps interpolatedPropsShared =
|
|
(newProps != nullptr ? cloneProps(context, newProps, newProps->rawProps)
|
|
: cloneProps(context, newProps, {}));
|
|
#else
|
|
SharedProps interpolatedPropsShared = cloneProps(context, newProps, {});
|
|
#endif
|
|
|
|
if (ConcreteShadowNode::BaseTraits().check(
|
|
ShadowNodeTraits::Trait::ViewKind)) {
|
|
interpolateViewProps(
|
|
animationProgress, props, newProps, interpolatedPropsShared);
|
|
}
|
|
|
|
return interpolatedPropsShared;
|
|
};
|
|
|
|
virtual State::Shared createInitialState(
|
|
ShadowNodeFragment const &fragment,
|
|
ShadowNodeFamily::Shared const &family) const override {
|
|
if (std::is_same<ConcreteStateData, StateData>::value) {
|
|
// Default case: Returning `null` for nodes that don't use `State`.
|
|
return nullptr;
|
|
}
|
|
|
|
return std::make_shared<ConcreteState>(
|
|
std::make_shared<ConcreteStateData const>(
|
|
ConcreteShadowNode::initialStateData(
|
|
fragment, ShadowNodeFamilyFragment::build(*family), *this)),
|
|
family);
|
|
}
|
|
|
|
virtual State::Shared createState(
|
|
ShadowNodeFamily const &family,
|
|
StateData::Shared const &data) const override {
|
|
if (std::is_same<ConcreteStateData, StateData>::value) {
|
|
// Default case: Returning `null` for nodes that don't use `State`.
|
|
return nullptr;
|
|
}
|
|
|
|
react_native_assert(data && "Provided `data` is nullptr.");
|
|
|
|
return std::make_shared<ConcreteState const>(
|
|
std::static_pointer_cast<ConcreteStateData const>(data),
|
|
*family.getMostRecentState());
|
|
}
|
|
|
|
ShadowNodeFamily::Shared createFamily(
|
|
ShadowNodeFamilyFragment const &fragment,
|
|
SharedEventTarget eventTarget) const override {
|
|
auto eventEmitter = std::make_shared<ConcreteEventEmitter const>(
|
|
std::move(eventTarget), fragment.tag, eventDispatcher_);
|
|
return std::make_shared<ShadowNodeFamily>(
|
|
ShadowNodeFamilyFragment{
|
|
fragment.tag, fragment.surfaceId, eventEmitter},
|
|
eventDispatcher_,
|
|
*this);
|
|
}
|
|
|
|
protected:
|
|
/*
|
|
* Called immediatelly after `ShadowNode` is created or cloned.
|
|
*
|
|
* Override this method to pass information from custom `ComponentDescriptor`
|
|
* to new instance of `ShadowNode`.
|
|
*
|
|
* Example usages:
|
|
* - Inject image manager to `ImageShadowNode` in
|
|
* `ImageComponentDescriptor`.
|
|
* - Set `ShadowNode`'s size from state in
|
|
* `ModalHostViewComponentDescriptor`.
|
|
*/
|
|
virtual void adopt(ShadowNode::Unshared const &shadowNode) const {
|
|
// Default implementation does nothing.
|
|
react_native_assert(
|
|
shadowNode->getComponentHandle() == getComponentHandle());
|
|
}
|
|
};
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|