New Props parsing infrastructure for perf improvements: visitor pattern vs random-map-access pattern (ViewProps, minus YogaLayoutableShadowNode)

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
This commit is contained in:
Joshua Gross
2022-06-15 23:37:34 -07:00
committed by Facebook GitHub Bot
parent dbe6fab063
commit 47280de85e
17 changed files with 791 additions and 287 deletions
@@ -15,134 +15,196 @@
namespace facebook {
namespace react {
bool AccessibilityProps::enablePropIteratorSetter = false;
AccessibilityProps::AccessibilityProps(
const PropsParserContext &context,
AccessibilityProps const &sourceProps,
RawProps const &rawProps)
: accessible(convertRawProp(
context,
rawProps,
"accessible",
sourceProps.accessible,
false)),
accessibilityState(convertRawProp(
context,
rawProps,
"accessibilityState",
sourceProps.accessibilityState,
{})),
accessibilityLabel(convertRawProp(
context,
rawProps,
"accessibilityLabel",
sourceProps.accessibilityLabel,
"")),
accessibilityLabelledBy(convertRawProp(
context,
rawProps,
"accessibilityLabelledBy",
sourceProps.accessibilityLabelledBy,
{})),
accessibilityLiveRegion(convertRawProp(
context,
rawProps,
"accessibilityLiveRegion",
sourceProps.accessibilityLiveRegion,
AccessibilityLiveRegion::None)),
accessibilityHint(convertRawProp(
context,
rawProps,
"accessibilityHint",
sourceProps.accessibilityHint,
"")),
accessibilityLanguage(convertRawProp(
context,
rawProps,
"accessibilityLanguage",
sourceProps.accessibilityLanguage,
"")),
accessibilityValue(convertRawProp(
context,
rawProps,
"accessibilityValue",
sourceProps.accessibilityValue,
{})),
accessibilityActions(convertRawProp(
context,
rawProps,
"accessibilityActions",
sourceProps.accessibilityActions,
{})),
accessibilityViewIsModal(convertRawProp(
context,
rawProps,
"accessibilityViewIsModal",
sourceProps.accessibilityViewIsModal,
false)),
accessibilityElementsHidden(convertRawProp(
context,
rawProps,
"accessibilityElementsHidden",
sourceProps.accessibilityElementsHidden,
false)),
accessibilityIgnoresInvertColors(convertRawProp(
context,
rawProps,
"accessibilityIgnoresInvertColors",
sourceProps.accessibilityIgnoresInvertColors,
false)),
onAccessibilityTap(convertRawProp(
context,
rawProps,
"onAccessibilityTap",
sourceProps.onAccessibilityTap,
{})),
onAccessibilityMagicTap(convertRawProp(
context,
rawProps,
"onAccessibilityMagicTap",
sourceProps.onAccessibilityMagicTap,
{})),
onAccessibilityEscape(convertRawProp(
context,
rawProps,
"onAccessibilityEscape",
sourceProps.onAccessibilityEscape,
{})),
onAccessibilityAction(convertRawProp(
context,
rawProps,
"onAccessibilityAction",
sourceProps.onAccessibilityAction,
{})),
importantForAccessibility(convertRawProp(
context,
rawProps,
"importantForAccessibility",
sourceProps.importantForAccessibility,
ImportantForAccessibility::Auto)),
: accessible(
enablePropIteratorSetter ? sourceProps.accessible
: convertRawProp(
context,
rawProps,
"accessible",
sourceProps.accessible,
false)),
accessibilityState(
enablePropIteratorSetter ? sourceProps.accessibilityState
: convertRawProp(
context,
rawProps,
"accessibilityState",
sourceProps.accessibilityState,
{})),
accessibilityLabel(
enablePropIteratorSetter ? sourceProps.accessibilityLabel
: convertRawProp(
context,
rawProps,
"accessibilityLabel",
sourceProps.accessibilityLabel,
"")),
accessibilityLabelledBy(
enablePropIteratorSetter ? sourceProps.accessibilityLabelledBy
: convertRawProp(
context,
rawProps,
"accessibilityLabelledBy",
sourceProps.accessibilityLabelledBy,
{})),
accessibilityLiveRegion(
enablePropIteratorSetter ? sourceProps.accessibilityLiveRegion
: convertRawProp(
context,
rawProps,
"accessibilityLiveRegion",
sourceProps.accessibilityLiveRegion,
AccessibilityLiveRegion::None)),
accessibilityHint(
enablePropIteratorSetter ? sourceProps.accessibilityHint
: convertRawProp(
context,
rawProps,
"accessibilityHint",
sourceProps.accessibilityHint,
"")),
accessibilityLanguage(
enablePropIteratorSetter ? sourceProps.accessibilityLanguage
: convertRawProp(
context,
rawProps,
"accessibilityLanguage",
sourceProps.accessibilityLanguage,
"")),
accessibilityValue(
enablePropIteratorSetter ? sourceProps.accessibilityValue
: convertRawProp(
context,
rawProps,
"accessibilityValue",
sourceProps.accessibilityValue,
{})),
accessibilityActions(
enablePropIteratorSetter ? sourceProps.accessibilityActions
: convertRawProp(
context,
rawProps,
"accessibilityActions",
sourceProps.accessibilityActions,
{})),
accessibilityViewIsModal(
enablePropIteratorSetter ? sourceProps.accessibilityViewIsModal
: convertRawProp(
context,
rawProps,
"accessibilityViewIsModal",
sourceProps.accessibilityViewIsModal,
false)),
accessibilityElementsHidden(
enablePropIteratorSetter
? sourceProps.accessibilityElementsHidden
: convertRawProp(
context,
rawProps,
"accessibilityElementsHidden",
sourceProps.accessibilityElementsHidden,
false)),
accessibilityIgnoresInvertColors(
enablePropIteratorSetter
? sourceProps.accessibilityIgnoresInvertColors
: convertRawProp(
context,
rawProps,
"accessibilityIgnoresInvertColors",
sourceProps.accessibilityIgnoresInvertColors,
false)),
onAccessibilityTap(
enablePropIteratorSetter ? sourceProps.onAccessibilityTap
: convertRawProp(
context,
rawProps,
"onAccessibilityTap",
sourceProps.onAccessibilityTap,
{})),
onAccessibilityMagicTap(
enablePropIteratorSetter ? sourceProps.onAccessibilityMagicTap
: convertRawProp(
context,
rawProps,
"onAccessibilityMagicTap",
sourceProps.onAccessibilityMagicTap,
{})),
onAccessibilityEscape(
enablePropIteratorSetter ? sourceProps.onAccessibilityEscape
: convertRawProp(
context,
rawProps,
"onAccessibilityEscape",
sourceProps.onAccessibilityEscape,
{})),
onAccessibilityAction(
enablePropIteratorSetter ? sourceProps.onAccessibilityAction
: convertRawProp(
context,
rawProps,
"onAccessibilityAction",
sourceProps.onAccessibilityAction,
{})),
importantForAccessibility(
enablePropIteratorSetter ? sourceProps.importantForAccessibility
: convertRawProp(
context,
rawProps,
"importantForAccessibility",
sourceProps.importantForAccessibility,
ImportantForAccessibility::Auto)),
testId(
convertRawProp(context, rawProps, "testID", sourceProps.testId, "")) {
// It is a (severe!) perf deoptimization to request props out-of-order.
// Thus, since we need to request the same prop twice here
// (accessibilityRole) we "must" do them subsequently here to prevent
// a regression. It is reasonable to ask if the `at` function can be improved;
// it probably can, but this is a fairly rare edge-case that (1) is easy-ish
// to work around here, and (2) would require very careful work to address
// this case and not regress the more common cases.
const auto *rawPropValue = rawProps.at("accessibilityRole", nullptr, nullptr);
AccessibilityTraits traits;
std::string roleString;
if (rawPropValue == nullptr || !rawPropValue->hasValue()) {
traits = AccessibilityTraits::None;
roleString = "";
} else {
fromRawValue(context, *rawPropValue, traits);
fromRawValue(context, *rawPropValue, roleString);
}
enablePropIteratorSetter ? sourceProps.testId
: convertRawProp(
context,
rawProps,
"testID",
sourceProps.testId,
"")) {}
accessibilityTraits = traits;
accessibilityRole = roleString;
void AccessibilityProps::setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value) {
switch (hash) {
RAW_SET_PROP_SWITCH_CASE_BASIC(accessible, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityState, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabel, std::string{""});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabelledBy, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityHint, std::string{""});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLanguage, std::string{""});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityValue, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityActions, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityViewIsModal, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityElementsHidden, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityIgnoresInvertColors, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityTap, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityMagicTap, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityEscape, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityAction, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(
importantForAccessibility, ImportantForAccessibility::Auto);
RAW_SET_PROP_SWITCH_CASE(testId, "testID", std::string{""});
case CONSTEXPR_RAW_PROPS_KEY_HASH("accessibilityRole"): {
AccessibilityTraits traits = AccessibilityTraits::None;
std::string roleString = "";
if (value.hasValue()) {
fromRawValue(context, value, traits);
fromRawValue(context, value, roleString);
}
accessibilityTraits = traits;
accessibilityRole = roleString;
return;
}
}
}
#pragma mark - DebugStringConvertible
@@ -24,6 +24,14 @@ class AccessibilityProps {
AccessibilityProps const &sourceProps,
RawProps const &rawProps);
void setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value);
static bool enablePropIteratorSetter;
#pragma mark - Props
bool accessible{false};
@@ -25,167 +25,331 @@ ViewProps::ViewProps(
bool shouldSetRawProps)
: YogaStylableProps(context, sourceProps, rawProps, shouldSetRawProps),
AccessibilityProps(context, sourceProps, rawProps),
opacity(convertRawProp(
context,
rawProps,
"opacity",
sourceProps.opacity,
(Float)1.0)),
foregroundColor(convertRawProp(
context,
rawProps,
"foregroundColor",
sourceProps.foregroundColor,
{})),
backgroundColor(convertRawProp(
context,
rawProps,
"backgroundColor",
sourceProps.backgroundColor,
{})),
borderRadii(convertRawProp(
context,
rawProps,
"border",
"Radius",
sourceProps.borderRadii,
{})),
borderColors(convertRawProp(
context,
rawProps,
"border",
"Color",
sourceProps.borderColors,
{})),
borderStyles(convertRawProp(
context,
rawProps,
"border",
"Style",
sourceProps.borderStyles,
{})),
shadowColor(convertRawProp(
context,
rawProps,
"shadowColor",
sourceProps.shadowColor,
{})),
shadowOffset(convertRawProp(
context,
rawProps,
"shadowOffset",
sourceProps.shadowOffset,
{})),
shadowOpacity(convertRawProp(
context,
rawProps,
"shadowOpacity",
sourceProps.shadowOpacity,
{})),
shadowRadius(convertRawProp(
context,
rawProps,
"shadowRadius",
sourceProps.shadowRadius,
{})),
transform(convertRawProp(
context,
rawProps,
"transform",
sourceProps.transform,
{})),
backfaceVisibility(convertRawProp(
context,
rawProps,
"backfaceVisibility",
sourceProps.backfaceVisibility,
{})),
shouldRasterize(convertRawProp(
context,
rawProps,
"shouldRasterize",
sourceProps.shouldRasterize,
{})),
opacity(
Props::enablePropIteratorSetter ? sourceProps.opacity
: convertRawProp(
context,
rawProps,
"opacity",
sourceProps.opacity,
(Float)1.0)),
foregroundColor(
Props::enablePropIteratorSetter ? sourceProps.foregroundColor
: convertRawProp(
context,
rawProps,
"foregroundColor",
sourceProps.foregroundColor,
{})),
backgroundColor(
Props::enablePropIteratorSetter ? sourceProps.backgroundColor
: convertRawProp(
context,
rawProps,
"backgroundColor",
sourceProps.backgroundColor,
{})),
borderRadii(
Props::enablePropIteratorSetter ? sourceProps.borderRadii
: convertRawProp(
context,
rawProps,
"border",
"Radius",
sourceProps.borderRadii,
{})),
borderColors(
Props::enablePropIteratorSetter ? sourceProps.borderColors
: convertRawProp(
context,
rawProps,
"border",
"Color",
sourceProps.borderColors,
{})),
borderStyles(
Props::enablePropIteratorSetter ? sourceProps.borderStyles
: convertRawProp(
context,
rawProps,
"border",
"Style",
sourceProps.borderStyles,
{})),
shadowColor(
Props::enablePropIteratorSetter ? sourceProps.shadowColor
: convertRawProp(
context,
rawProps,
"shadowColor",
sourceProps.shadowColor,
{})),
shadowOffset(
Props::enablePropIteratorSetter ? sourceProps.shadowOffset
: convertRawProp(
context,
rawProps,
"shadowOffset",
sourceProps.shadowOffset,
{})),
shadowOpacity(
Props::enablePropIteratorSetter ? sourceProps.shadowOpacity
: convertRawProp(
context,
rawProps,
"shadowOpacity",
sourceProps.shadowOpacity,
{})),
shadowRadius(
Props::enablePropIteratorSetter ? sourceProps.shadowRadius
: convertRawProp(
context,
rawProps,
"shadowRadius",
sourceProps.shadowRadius,
{})),
transform(
Props::enablePropIteratorSetter ? sourceProps.transform
: convertRawProp(
context,
rawProps,
"transform",
sourceProps.transform,
{})),
backfaceVisibility(
Props::enablePropIteratorSetter ? sourceProps.backfaceVisibility
: convertRawProp(
context,
rawProps,
"backfaceVisibility",
sourceProps.backfaceVisibility,
{})),
shouldRasterize(
Props::enablePropIteratorSetter ? sourceProps.shouldRasterize
: convertRawProp(
context,
rawProps,
"shouldRasterize",
sourceProps.shouldRasterize,
{})),
zIndex(
convertRawProp(context, rawProps, "zIndex", sourceProps.zIndex, {})),
pointerEvents(convertRawProp(
context,
rawProps,
"pointerEvents",
sourceProps.pointerEvents,
{})),
hitSlop(convertRawProp(
context,
rawProps,
"hitSlop",
sourceProps.hitSlop,
{})),
onLayout(convertRawProp(
context,
rawProps,
"onLayout",
sourceProps.onLayout,
{})),
events(convertRawProp(context, rawProps, sourceProps.events, {})),
collapsable(convertRawProp(
context,
rawProps,
"collapsable",
sourceProps.collapsable,
true)),
removeClippedSubviews(convertRawProp(
context,
rawProps,
"removeClippedSubviews",
sourceProps.removeClippedSubviews,
false))
Props::enablePropIteratorSetter ? sourceProps.zIndex
: convertRawProp(
context,
rawProps,
"zIndex",
sourceProps.zIndex,
{})),
pointerEvents(
Props::enablePropIteratorSetter ? sourceProps.pointerEvents
: convertRawProp(
context,
rawProps,
"pointerEvents",
sourceProps.pointerEvents,
{})),
hitSlop(
Props::enablePropIteratorSetter ? sourceProps.hitSlop
: convertRawProp(
context,
rawProps,
"hitSlop",
sourceProps.hitSlop,
{})),
onLayout(
Props::enablePropIteratorSetter ? sourceProps.onLayout
: convertRawProp(
context,
rawProps,
"onLayout",
sourceProps.onLayout,
{})),
events(
Props::enablePropIteratorSetter
? sourceProps.events
: convertRawProp(context, rawProps, sourceProps.events, {})),
collapsable(
Props::enablePropIteratorSetter ? sourceProps.collapsable
: convertRawProp(
context,
rawProps,
"collapsable",
sourceProps.collapsable,
true)),
removeClippedSubviews(
Props::enablePropIteratorSetter
? sourceProps.removeClippedSubviews
: convertRawProp(
context,
rawProps,
"removeClippedSubviews",
sourceProps.removeClippedSubviews,
false))
#ifdef ANDROID
,
elevation(convertRawProp(
context,
rawProps,
"elevation",
sourceProps.elevation,
{})),
nativeBackground(convertRawProp(
context,
rawProps,
"nativeBackgroundAndroid",
sourceProps.nativeBackground,
{})),
nativeForeground(convertRawProp(
context,
rawProps,
"nativeForegroundAndroid",
sourceProps.nativeForeground,
{})),
focusable(convertRawProp(
context,
rawProps,
"focusable",
sourceProps.focusable,
{})),
hasTVPreferredFocus(convertRawProp(
context,
rawProps,
"hasTVPreferredFocus",
sourceProps.hasTVPreferredFocus,
{})),
needsOffscreenAlphaCompositing(convertRawProp(
context,
rawProps,
"needsOffscreenAlphaCompositing",
sourceProps.needsOffscreenAlphaCompositing,
{})),
renderToHardwareTextureAndroid(convertRawProp(
context,
rawProps,
"renderToHardwareTextureAndroid",
sourceProps.renderToHardwareTextureAndroid,
{}))
elevation(
Props::enablePropIteratorSetter ? sourceProps.elevation
: convertRawProp(
context,
rawProps,
"elevation",
sourceProps.elevation,
{})),
nativeBackground(
Props::enablePropIteratorSetter ? sourceProps.nativeBackground
: convertRawProp(
context,
rawProps,
"nativeBackgroundAndroid",
sourceProps.nativeBackground,
{})),
nativeForeground(
Props::enablePropIteratorSetter ? sourceProps.nativeForeground
: convertRawProp(
context,
rawProps,
"nativeForegroundAndroid",
sourceProps.nativeForeground,
{})),
focusable(
Props::enablePropIteratorSetter ? sourceProps.focusable
: convertRawProp(
context,
rawProps,
"focusable",
sourceProps.focusable,
{})),
hasTVPreferredFocus(
Props::enablePropIteratorSetter ? sourceProps.hasTVPreferredFocus
: convertRawProp(
context,
rawProps,
"hasTVPreferredFocus",
sourceProps.hasTVPreferredFocus,
{})),
needsOffscreenAlphaCompositing(
Props::enablePropIteratorSetter
? sourceProps.needsOffscreenAlphaCompositing
: convertRawProp(
context,
rawProps,
"needsOffscreenAlphaCompositing",
sourceProps.needsOffscreenAlphaCompositing,
{})),
renderToHardwareTextureAndroid(
Props::enablePropIteratorSetter
? sourceProps.renderToHardwareTextureAndroid
: convertRawProp(
context,
rawProps,
"renderToHardwareTextureAndroid",
sourceProps.renderToHardwareTextureAndroid,
{}))
#endif
{};
#define VIEW_EVENT_CASE(eventType, eventString) \
case CONSTEXPR_RAW_PROPS_KEY_HASH(eventString): { \
ViewEvents defaultViewEvents{}; \
events[eventType] = ({ \
bool res = defaultViewEvents[eventType]; \
if (value.hasValue()) { \
fromRawValue(context, value, res); \
} \
res; \
}); \
return; \
}
void ViewProps::setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value) {
// All Props structs setProp methods must always, unconditionally,
// call all super::setProp methods, since multiple structs may
// reuse the same values.
YogaStylableProps::setProp(context, hash, propName, value);
AccessibilityProps::setProp(context, hash, propName, value);
switch (hash) {
RAW_SET_PROP_SWITCH_CASE_BASIC(opacity, (Float)1.0);
RAW_SET_PROP_SWITCH_CASE_BASIC(foregroundColor, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(backgroundColor, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowRadius, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(transform, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(backfaceVisibility, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(shouldRasterize, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(zIndex, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(pointerEvents, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable, true);
RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews, false);
// events field
VIEW_EVENT_CASE(ViewEvents::Offset::PointerEnter, "onPointerEnter");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerMove, "onPointerMove");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerLeave, "onPointerLeave");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerEnter2, "onPointerEnter2");
VIEW_EVENT_CASE(
ViewEvents::Offset::PointerEnter2Capture, "onPointerEnter2Capture");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerMove2, "onPointerMove2");
VIEW_EVENT_CASE(
ViewEvents::Offset::PointerMove2Capture, "onPointerMove2Capture");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerLeave2, "onPointerLeave2");
VIEW_EVENT_CASE(
ViewEvents::Offset::PointerLeave2Capture, "onPointerLeave2Capture");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerOver, "onPointerOver");
VIEW_EVENT_CASE(ViewEvents::Offset::PointerOut, "onPointerOut");
VIEW_EVENT_CASE(
ViewEvents::Offset::MoveShouldSetResponder, "onMoveShouldSetResponder");
VIEW_EVENT_CASE(
ViewEvents::Offset::MoveShouldSetResponderCapture,
"onMoveShouldSetResponderCapture");
VIEW_EVENT_CASE(
ViewEvents::Offset::StartShouldSetResponder,
"onStartShouldSetResponder");
VIEW_EVENT_CASE(
ViewEvents::Offset::StartShouldSetResponderCapture,
"onStartShouldSetResponderCapture");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderGrant, "onResponderGrant");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderReject, "onResponderReject");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderStart, "onResponderStart");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderEnd, "onResponderEnd");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderRelease, "onResponderRelease");
VIEW_EVENT_CASE(ViewEvents::Offset::ResponderMove, "ResponderMove");
VIEW_EVENT_CASE(
ViewEvents::Offset::ResponderTerminate, "onResponderTerminate");
VIEW_EVENT_CASE(
ViewEvents::Offset::ResponderTerminationRequest,
"onResponderTerminationRequest");
VIEW_EVENT_CASE(
ViewEvents::Offset::ShouldBlockNativeResponder,
"onShouldBlockNativeResponder");
VIEW_EVENT_CASE(ViewEvents::Offset::TouchStart, "onTouchStart");
VIEW_EVENT_CASE(ViewEvents::Offset::TouchMove, "onTouchMove");
VIEW_EVENT_CASE(ViewEvents::Offset::TouchEnd, "onTouchEnd");
VIEW_EVENT_CASE(ViewEvents::Offset::TouchCancel, "onTouchCancel");
#ifdef ANDROID
RAW_SET_PROP_SWITCH_CASE_BASIC(elevation, {});
RAW_SET_PROP_SWITCH_CASE(nativeBackground, "nativeBackgroundAndroid", {});
RAW_SET_PROP_SWITCH_CASE(nativeForeground, "nativeForegroundAndroid", {});
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing, false);
RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid, false);
#endif
// BorderRadii
SET_CASCADED_RECTANGLE_CORNERS(borderRadii, "border", "Radius", value);
SET_CASCADED_RECTANGLE_EDGES(borderColors, "border", "Color", value);
SET_CASCADED_RECTANGLE_EDGES(borderStyles, "border", "Style", value);
}
}
#pragma mark - Convenience Methods
static BorderRadii ensureNoOverlap(BorderRadii const &radii, Size const &size) {
@@ -35,6 +35,12 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {
RawProps const &rawProps,
bool shouldSetRawProps = true);
void setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value);
#pragma mark - Props
// Color
@@ -301,6 +301,7 @@ static inline YGStyle convertRawProp(
return yogaStyle;
}
// This can be deleted when non-iterator ViewProp parsing is deleted
template <typename T>
static inline CascadedRectangleCorners<T> convertRawProp(
const PropsParserContext &context,
@@ -465,6 +466,7 @@ static inline CascadedRectangleEdges<T> convertRawProp(
return result;
}
// This can be deleted when non-iterator ViewProp parsing is deleted
static inline ViewEvents convertRawProp(
const PropsParserContext &context,
RawProps const &rawProps,
@@ -109,7 +109,21 @@ class ConcreteComponentDescriptor : public ComponentDescriptor {
rawProps.parse(rawPropsParser_, context);
return ShadowNodeT::Props(context, rawProps, props);
// 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(
@@ -47,6 +47,7 @@ class ConcreteShadowNode : public BaseShadowNodeT {
using ConcreteProps = PropsT;
using SharedConcreteProps = std::shared_ptr<PropsT const>;
using UnsharedConcreteProps = std::shared_ptr<PropsT>;
using ConcreteEventEmitter = EventEmitterT;
using SharedConcreteEventEmitter = std::shared_ptr<EventEmitterT const>;
using SharedConcreteShadowNode = std::shared_ptr<ConcreteShadowNode const>;
@@ -69,11 +70,11 @@ class ConcreteShadowNode : public BaseShadowNodeT {
return BaseShadowNodeT::BaseTraits();
}
static SharedConcreteProps Props(
static UnsharedConcreteProps Props(
const PropsParserContext &context,
RawProps const &rawProps,
SharedProps const &baseProps = nullptr) {
return std::make_shared<PropsT const>(
return std::make_shared<PropsT>(
context,
baseProps ? static_cast<PropsT const &>(*baseProps) : PropsT(),
rawProps);
+24 -7
View File
@@ -13,17 +13,21 @@
namespace facebook {
namespace react {
bool Props::enablePropIteratorSetter = false;
Props::Props(
const PropsParserContext &context,
const Props &sourceProps,
const RawProps &rawProps,
const bool shouldSetRawProps)
: nativeId(convertRawProp(
context,
rawProps,
"nativeID",
sourceProps.nativeId,
{})),
: nativeId(
enablePropIteratorSetter ? sourceProps.nativeId
: convertRawProp(
context,
rawProps,
"nativeID",
sourceProps.nativeId,
{})),
revision(sourceProps.revision + 1)
#ifdef ANDROID
,
@@ -31,7 +35,20 @@ Props::Props(
shouldSetRawProps ? (folly::dynamic)rawProps
: /* null */ folly::dynamic())
#endif
{};
{
}
void Props::setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value) {
switch (hash) {
case CONSTEXPR_RAW_PROPS_KEY_HASH("nativeID"):
fromRawValue(context, value, nativeId, {});
return;
}
}
} // namespace react
} // namespace facebook
+19
View File
@@ -9,6 +9,7 @@
#include <folly/dynamic.h>
#include <react/renderer/core/PropsMacros.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/RawProps.h>
#include <react/renderer/core/ReactPrimitives.h>
@@ -37,6 +38,24 @@ class Props : public virtual Sealable, public virtual DebugStringConvertible {
bool shouldSetRawProps = true);
virtual ~Props() = default;
static bool enablePropIteratorSetter;
/**
* Set a prop value via iteration (see enableIterator above).
* If setProp is defined for a particular props struct, it /must/
* be called every time setProp is called on the hierarchy.
* For example, ViewProps overrides setProp and so ViewProps must
* explicitly call Props::setProp every time ViewProps::setProp is
* called. This is because a single prop from JS can be reused
* multiple times for different values in the hierarchy. For example, if
* ViewProps uses "propX", Props may also use "propX".
*/
void setProp(
const PropsParserContext &context,
RawPropsPropNameHash hash,
const char *propName,
RawValue const &value);
std::string nativeId;
/*
@@ -0,0 +1,90 @@
/*
* 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/RawPropsPrimitives.h>
// Get hash at compile-time. sizeof(str) - 1 == strlen
// Auto-formatting makes this more ugly than it has to be, but it works.
// The pragma is to ignore warnings about variable shadowing of `len` and/or
// `hash`, which are technically shadowing but it doesn't matter since they're
// constexpr'd.
#define CONSTEXPR_RAW_PROPS_KEY_HASH(s) \
({ \
_Pragma("clang diagnostic push") _Pragma( \
"clang diagnostic ignored \"-Wshadow\"") constexpr RawPropsPropNameLength \
len = sizeof(s) - 1; \
constexpr RawPropsPropNameHash hash = folly::hash::fnv32_buf(s, len); \
hash; \
_Pragma("clang diagnostic pop") \
})
#define RAW_PROPS_KEY_HASH(s) folly::hash::fnv32_buf(s, std::strlen(s))
// Convenience for building setProps switch statements.
// This injects `fromRawValue` into source; each file that uses
// this macro must import the proper, respective headers required.
#define RAW_SET_PROP_SWITCH_CASE(field, jsPropName, defaultValue) \
case CONSTEXPR_RAW_PROPS_KEY_HASH(jsPropName): \
fromRawValue(context, value, field, defaultValue); \
return;
// Convenience for building setProps switch statements where the field name is
// the same as the string identifier
#define RAW_SET_PROP_SWITCH_CASE_BASIC(field, defaultValue) \
RAW_SET_PROP_SWITCH_CASE(field, #field, defaultValue)
#define CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, field, fieldNameString, value) \
case CONSTEXPR_RAW_PROPS_KEY_HASH(fieldNameString): { \
if (!value.hasValue()) { \
decltype(struct) defaultValues{}; \
struct.field = defaultValues.field; \
return; \
} \
fromRawValue(context, value, struct.field); \
return; \
}
#define SET_CASCADED_RECTANGLE_CORNERS(struct, prefix, suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, topLeft, prefix "TopLeft" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, topRight, prefix "TopRight" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, bottomLeft, prefix "BottomLeft" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, bottomRight, prefix "BottomRight" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, topStart, prefix "TopStart" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, topEnd, prefix "TopEnd" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, bottomStart, prefix "BottomStart" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, bottomEnd, prefix "BottomEnd" suffix, rawValue)
#define SET_CASCADED_RECTANGLE_EDGES(struct, prefix, suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, left, prefix "Left" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, right, prefix "Right" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, top, prefix "Top" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, bottom, prefix "Bottom" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, start, prefix "Start" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, end, prefix "End" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, horizontal, prefix "Horizontal" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, vertical, prefix "Vertical" suffix, rawValue) \
CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \
struct, all, prefix "" suffix, rawValue)
@@ -93,5 +93,12 @@ const RawValue *RawProps::at(
return parser_->at(*this, RawPropsKey{prefix, name, suffix});
}
void RawProps::iterateOverValues(
std::function<
void(RawPropsPropNameHash, const char *, RawValue const &)> const &fn)
const {
return parser_->iterateOverValues(*this, fn);
}
} // namespace react
} // namespace facebook
@@ -97,6 +97,15 @@ class RawProps final {
const RawValue *at(char const *name, char const *prefix, char const *suffix)
const noexcept;
/**
* Iterator functions: for when you want to iterate over values in-order
* instead of using `at` to access values randomly.
*/
void iterateOverValues(
std::function<
void(RawPropsPropNameHash, const char *, RawValue const &)> const &fn)
const;
private:
friend class RawPropsParser;
@@ -8,6 +8,7 @@
#include "RawPropsParser.h"
#include <folly/Likely.h>
#include <folly/hash/Hash.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/RawProps.h>
@@ -99,6 +100,13 @@ void RawPropsParser::preparse(RawProps const &rawProps) const noexcept {
// Resetting the cursor, the next increment will give `0`.
rawProps.keyIndexCursor_ = static_cast<int>(keyCount - 1);
// If the Props constructor doesn't use ::at at all, we might be
// able to skip this entirely (in those cases, the Props struct probably
// uses setProp instead).
if (keyCount == 0) {
return;
}
switch (rawProps.mode_) {
case RawProps::Mode::Empty:
return;
@@ -123,6 +131,7 @@ void RawPropsParser::preparse(RawProps const &rawProps) const noexcept {
auto keyIndex = nameToIndex_.at(
name.data(), static_cast<RawPropsPropNameLength>(name.size()));
if (keyIndex == kRawPropsValueIndexEmpty) {
continue;
}
@@ -145,6 +154,7 @@ void RawPropsParser::preparse(RawProps const &rawProps) const noexcept {
auto keyIndex = nameToIndex_.at(
name.data(), static_cast<RawPropsPropNameLength>(name.size()));
if (keyIndex == kRawPropsValueIndexEmpty) {
continue;
}
@@ -158,5 +168,58 @@ void RawPropsParser::preparse(RawProps const &rawProps) const noexcept {
}
}
/**
* To be used by RawProps only. Value iterator functions.
*/
void RawPropsParser::iterateOverValues(
RawProps const &rawProps,
std::function<
void(RawPropsPropNameHash, const char *, RawValue const &)> const
&visit) const {
switch (rawProps.mode_) {
case RawProps::Mode::Empty:
return;
case RawProps::Mode::JSI: {
auto &runtime = *rawProps.runtime_;
if (!rawProps.value_.isObject()) {
LOG(ERROR) << "Preparse props: rawProps value is not object";
}
react_native_assert(rawProps.value_.isObject());
auto object = rawProps.value_.asObject(runtime);
auto names = object.getPropertyNames(runtime);
auto count = names.size(runtime);
for (size_t i = 0; i < count; i++) {
auto nameValue = names.getValueAtIndex(runtime, i).getString(runtime);
auto value = object.getProperty(runtime, nameValue);
auto name = nameValue.utf8(runtime);
auto nameHash = RAW_PROPS_KEY_HASH(name.c_str());
auto rawValue = RawValue(jsi::dynamicFromValue(runtime, value));
visit(nameHash, name.c_str(), rawValue);
}
break;
}
case RawProps::Mode::Dynamic: {
auto const &dynamic = rawProps.dynamic_;
for (auto const &pair : dynamic.items()) {
auto name = pair.first.getString();
auto nameHash = RAW_PROPS_KEY_HASH(name.c_str());
auto rawValue = RawValue{pair.second};
visit(nameHash, name.c_str(), rawValue);
}
break;
}
}
}
} // namespace react
} // namespace facebook
@@ -76,6 +76,15 @@ class RawPropsParser final {
RawValue const *at(RawProps const &rawProps, RawPropsKey const &key)
const noexcept;
/**
* To be used by RawProps only. Value iterator functions.
*/
void iterateOverValues(
RawProps const &rawProps,
std::function<
void(RawPropsPropNameHash, const char *, RawValue const &)> const &fn)
const;
mutable butter::small_vector<RawPropsKey, kNumberOfPropsPerComponentSoftCap>
keys_{};
mutable RawPropsKeyMap nameToIndex_{};
@@ -7,6 +7,7 @@
#pragma once
#include <folly/hash/Hash.h>
#include <limits>
namespace facebook {
@@ -20,6 +21,7 @@ static_assert(
sizeof(RawPropsValueIndex) == 1,
"RawPropsValueIndex must be one byte size.");
using RawPropsPropNameLength = uint8_t;
using RawPropsPropNameHash = uint32_t;
/*
* The number which is *usually* bigger than a number of explicitly specified
@@ -12,6 +12,8 @@
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/renderer/core/RawPropsPrimitives.h>
#include <react/debug/react_native_assert.h>
namespace facebook {
@@ -20,6 +20,35 @@
namespace facebook {
namespace react {
/**
* Use this only when a prop update has definitely been sent from JS;
* essentially, cases where rawValue is virtually guaranteed to not be a
* nullptr.
*/
template <typename T>
void fromRawValue(
const PropsParserContext &context,
RawValue const &rawValue,
T &result,
T defaultValue) {
if (!rawValue.hasValue()) {
result = std::move(defaultValue);
return;
}
fromRawValue(context, rawValue, result);
}
template <typename T>
void fromRawValue(
const PropsParserContext &context,
RawValue const &rawValue,
std::optional<T> &result) {
T res{};
fromRawValue(context, rawValue, res);
result = std::optional<T>(res);
}
template <typename T>
void fromRawValue(
const PropsParserContext &context,