Skip to content

Commit 7620e3a

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Reduce animation registry copying and allocation
Summary: For every raw update, `AnimatedPropsRegistry::update` copied the incoming props before merging them, and it built a full typed view-props snapshot even when no typed prop was animated. It now merges from the incoming `folly::dynamic` without copying it and allocates typed props only when a typed update arrives. The merge semantics (Folly's merge-patch) and the caller's input are unchanged. On a low-end Android phone (4× Cortex-A53) with 12 views animating opacity, the registry update per frame dropped from about 0.22 ms to 0.13 ms. The new registry tests are excluded from React-Fabric's CocoaPods and Swift Package Manager production sources. ## Changelog: [General] [Performance] - Reduce animation registry copying and allocation Differential Revision: D122570619
1 parent 0d44917 commit 7620e3a

14 files changed

Lines changed: 335 additions & 98 deletions

‎packages/react-native/Package.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ let reactFabric = RNTarget(
459459
path: "ReactCommon/react/renderer",
460460
excludedPaths: [
461461
"animated/tests",
462+
"animationbackend/tests",
462463
"animations/tests",
463464
"attributedstring/tests",
464465
"core/tests",

‎packages/react-native/ReactCommon/React-Fabric.podspec‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Pod::Spec.new do |s|
7373

7474
s.subspec "animationbackend" do |ss|
7575
ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}")
76+
ss.exclude_files = "react/renderer/animationbackend/tests"
7677
ss.header_dir = "react/renderer/animationbackend"
7778
end
7879

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,27 @@ void AnimatedPropsRegistry::update(
3232
.first;
3333
}
3434
auto& snapshot = it->second;
35-
auto& viewProps = snapshot->props;
36-
3735
if (animatedProps.rawProps) {
3836
const auto& newRawProps = *animatedProps.rawProps;
3937
auto& currentRawProps = snapshot->rawProps;
4038

4139
if (currentRawProps) {
42-
auto newRawPropsDynamic = newRawProps.toDynamic();
43-
currentRawProps->merge_patch(newRawPropsDynamic);
40+
if (const auto* dynamic = newRawProps.getDynamic()) {
41+
currentRawProps->merge_patch(*dynamic);
42+
} else {
43+
currentRawProps->merge_patch(newRawProps.toDynamic());
44+
}
4445
} else {
4546
currentRawProps =
4647
std::make_unique<folly::dynamic>(newRawProps.toDynamic());
4748
}
4849
}
50+
if (!animatedProps.props.empty() && !snapshot->props) {
51+
snapshot->props = std::make_unique<BaseViewProps>();
52+
}
4953
for (const auto& animatedProp : animatedProps.props) {
5054
snapshot->propNames.insert(animatedProp->propName);
51-
cloneProp(viewProps, *animatedProp);
55+
cloneProp(*snapshot->props, *animatedProp);
5256
}
5357
}
5458
}
@@ -83,9 +87,14 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
8387
currentSnapshot->rawProps = std::move(propsSnapshot->rawProps);
8488
}
8589
}
86-
for (auto& propName : propsSnapshot->propNames) {
87-
currentSnapshot->propNames.insert(propName);
88-
updateProp(propName, currentSnapshot->props, *propsSnapshot);
90+
if (!currentSnapshot->props) {
91+
currentSnapshot->props = std::move(propsSnapshot->props);
92+
currentSnapshot->propNames = std::move(propsSnapshot->propNames);
93+
} else {
94+
for (auto& propName : propsSnapshot->propNames) {
95+
currentSnapshot->propNames.insert(propName);
96+
updateProp(propName, *currentSnapshot->props, *propsSnapshot);
97+
}
8998
}
9099
}
91100
}

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h‎

Lines changed: 81 additions & 81 deletions
Large diffs are not rendered by default.
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <gtest/gtest.h>
9+
#include <hermes/hermes.h>
10+
#include <react/renderer/animationbackend/AnimatedPropsBuilder.h>
11+
#include <react/renderer/animationbackend/AnimatedPropsRegistry.h>
12+
#include <react/renderer/components/view/ViewComponentDescriptor.h>
13+
#include <react/renderer/element/ComponentBuilder.h>
14+
#include <react/renderer/element/Element.h>
15+
#include <react/renderer/element/testUtils.h>
16+
#include <react/utils/ContextContainer.h>
17+
18+
namespace facebook::react {
19+
20+
class AnimatedPropsRegistryTest : public ::testing::Test {
21+
protected:
22+
ComponentBuilder builder =
23+
simpleComponentBuilder(std::make_shared<ContextContainer>());
24+
std::shared_ptr<ViewShadowNode> node =
25+
builder.build(Element<ViewShadowNode>().surfaceId(1).tag(10));
26+
AnimatedPropsRegistry registry;
27+
28+
void SetUp() override {
29+
registry.initializeSurface(1);
30+
}
31+
32+
void update(AnimatedProps props) {
33+
std::vector<AnimationMutations> batches(1);
34+
batches[0].batch.push_back(
35+
AnimationMutation{
36+
.tag = 10,
37+
.family = node->getFamilyShared(),
38+
.props = std::move(props)});
39+
registry.update(batches);
40+
}
41+
42+
void updateRaw(folly::dynamic props) {
43+
update({{}, std::make_unique<RawProps>(std::move(props))});
44+
}
45+
46+
void expectRawProps(folly::dynamic expected) {
47+
EXPECT_EQ(*snapshot().rawProps, expected);
48+
}
49+
50+
PropsSnapshot& snapshot() {
51+
return *registry.getMap(1).second.at(10);
52+
}
53+
};
54+
55+
TEST_F(AnimatedPropsRegistryTest, rawUpdatesDoNotAllocateTypedProps) {
56+
for (int frame = 0; frame < 10; frame++) {
57+
updateRaw(folly::dynamic::object("opacity", frame / 10.0));
58+
auto& props = snapshot();
59+
EXPECT_EQ(props.props, nullptr);
60+
EXPECT_TRUE(props.propNames.empty());
61+
EXPECT_EQ((*props.rawProps)["opacity"], frame / 10.0);
62+
}
63+
}
64+
65+
TEST_F(AnimatedPropsRegistryTest, updatePreservesInputForSynchronousMount) {
66+
folly::dynamic rawProps = folly::dynamic::object("opacity", 0.25)(
67+
"transform",
68+
folly::dynamic::array(folly::dynamic::object("translateX", 10)));
69+
std::vector<AnimationMutations> batches(1);
70+
batches[0].batch.push_back(
71+
AnimationMutation{
72+
.tag = 10,
73+
.family = node->getFamilyShared(),
74+
.props = AnimatedProps{{}, std::make_unique<RawProps>(rawProps)}});
75+
registry.update(batches);
76+
registry.update(batches);
77+
EXPECT_EQ(batches[0].batch[0].props.rawProps->toDynamic(), rawProps);
78+
EXPECT_EQ(*snapshot().rawProps, rawProps);
79+
}
80+
81+
TEST_F(AnimatedPropsRegistryTest, mergesPendingUpdatesRecursively) {
82+
updateRaw(
83+
folly::dynamic::object("opacity", 0.25)(
84+
"shadowOffset", folly::dynamic::object("width", 2)("height", 3)));
85+
updateRaw(
86+
folly::dynamic::object(
87+
"shadowOffset", folly::dynamic::object("width", 7)));
88+
expectRawProps(
89+
folly::dynamic::object("opacity", 0.25)(
90+
"shadowOffset", folly::dynamic::object("width", 7)("height", 3)));
91+
}
92+
93+
TEST_F(AnimatedPropsRegistryTest, mergesCommittedUpdatesRecursively) {
94+
updateRaw(
95+
folly::dynamic::object("opacity", 0.25)(
96+
"shadowOffset", folly::dynamic::object("width", 2)("height", 3)));
97+
snapshot();
98+
updateRaw(
99+
folly::dynamic::object(
100+
"shadowOffset", folly::dynamic::object("width", 7)));
101+
expectRawProps(
102+
folly::dynamic::object("opacity", 0.25)(
103+
"shadowOffset", folly::dynamic::object("width", 7)("height", 3)));
104+
}
105+
106+
TEST_F(AnimatedPropsRegistryTest, nullDeletesPendingProps) {
107+
updateRaw(
108+
folly::dynamic::object("opacity", 0.25)(
109+
"shadowOffset", folly::dynamic::object("width", 2)("height", 3)));
110+
updateRaw(
111+
folly::dynamic::object("opacity", nullptr)(
112+
"shadowOffset", folly::dynamic::object("width", nullptr)));
113+
expectRawProps(
114+
folly::dynamic::object(
115+
"shadowOffset", folly::dynamic::object("height", 3)));
116+
}
117+
118+
TEST_F(AnimatedPropsRegistryTest, nullDeletesCommittedProps) {
119+
updateRaw(
120+
folly::dynamic::object("opacity", 0.25)(
121+
"shadowOffset", folly::dynamic::object("width", 2)("height", 3)));
122+
snapshot();
123+
updateRaw(
124+
folly::dynamic::object("opacity", nullptr)(
125+
"shadowOffset", folly::dynamic::object("width", nullptr)));
126+
expectRawProps(
127+
folly::dynamic::object(
128+
"shadowOffset", folly::dynamic::object("height", 3)));
129+
}
130+
131+
TEST_F(AnimatedPropsRegistryTest, arraysReplaceInsteadOfMerging) {
132+
updateRaw(
133+
folly::dynamic::object(
134+
"transform",
135+
folly::dynamic::array(
136+
folly::dynamic::object("translateX", 2),
137+
folly::dynamic::object("scale", 3))));
138+
snapshot();
139+
auto transform =
140+
folly::dynamic::array(folly::dynamic::object("rotate", "1rad"));
141+
updateRaw(folly::dynamic::object("transform", transform));
142+
EXPECT_EQ((*snapshot().rawProps)["transform"], transform);
143+
}
144+
145+
TEST_F(AnimatedPropsRegistryTest, objectPatchReplacesScalarAndRemovesNulls) {
146+
updateRaw(folly::dynamic::object("value", 5));
147+
snapshot();
148+
updateRaw(
149+
folly::dynamic::object(
150+
"value", folly::dynamic::object("width", 2)("height", nullptr)));
151+
expectRawProps(
152+
folly::dynamic::object("value", folly::dynamic::object("width", 2)));
153+
updateRaw(folly::dynamic::object("value", 7));
154+
expectRawProps(folly::dynamic::object("value", 7));
155+
}
156+
157+
TEST_F(AnimatedPropsRegistryTest, typedPropsCanFollowRawProps) {
158+
updateRaw(folly::dynamic::object("opacity", 0.25));
159+
snapshot();
160+
AnimatedPropsBuilder builder;
161+
builder.setOpacity(0.75);
162+
update(builder.get());
163+
auto& props = snapshot();
164+
ASSERT_NE(props.props, nullptr);
165+
EXPECT_FLOAT_EQ(props.props->opacity, 0.75);
166+
EXPECT_TRUE(props.propNames.contains(OPACITY));
167+
EXPECT_EQ((*props.rawProps)["opacity"], 0.25);
168+
BaseViewProps viewProps;
169+
updateProp(OPACITY, viewProps, props);
170+
EXPECT_FLOAT_EQ(viewProps.opacity, 0.75);
171+
}
172+
173+
TEST_F(AnimatedPropsRegistryTest, rawPropsCanFollowTypedProps) {
174+
AnimatedPropsBuilder builder;
175+
builder.setOpacity(0.75);
176+
update(builder.get());
177+
snapshot();
178+
updateRaw(folly::dynamic::object("opacity", 0.25));
179+
auto& props = snapshot();
180+
ASSERT_NE(props.props, nullptr);
181+
EXPECT_FLOAT_EQ(props.props->opacity, 0.75);
182+
EXPECT_TRUE(props.propNames.contains(OPACITY));
183+
EXPECT_EQ((*props.rawProps)["opacity"], 0.25);
184+
}
185+
186+
TEST_F(AnimatedPropsRegistryTest, mergesTypedUpdatesAcrossCommits) {
187+
AnimatedPropsBuilder builder;
188+
builder.setOpacity(0.75);
189+
builder.setShadowRadius(2);
190+
update(builder.get());
191+
snapshot();
192+
builder.setOpacity(0.5);
193+
update(builder.get());
194+
auto& props = snapshot();
195+
ASSERT_NE(props.props, nullptr);
196+
EXPECT_FLOAT_EQ(props.props->opacity, 0.5);
197+
EXPECT_FLOAT_EQ(props.props->shadowRadius, 2);
198+
EXPECT_EQ(props.propNames.size(), 2);
199+
}
200+
201+
TEST_F(AnimatedPropsRegistryTest, jsiRawPropsAreConverted) {
202+
auto runtime = hermes::makeHermesRuntime();
203+
{
204+
auto object = jsi::Object(*runtime);
205+
object.setProperty(*runtime, "opacity", 0.25);
206+
update(
207+
{{},
208+
std::make_unique<RawProps>(*runtime, jsi::Value(*runtime, object))});
209+
object.setProperty(*runtime, "opacity", 0.5);
210+
update(
211+
{{},
212+
std::make_unique<RawProps>(*runtime, jsi::Value(*runtime, object))});
213+
}
214+
runtime.reset();
215+
expectRawProps(folly::dynamic::object("opacity", 0.5));
216+
EXPECT_EQ(snapshot().props, nullptr);
217+
}
218+
219+
TEST_F(AnimatedPropsRegistryTest, stoppedSurfacesIgnoreUpdates) {
220+
registry.clearOnSurfaceStop(1);
221+
updateRaw(folly::dynamic::object("opacity", 0.5));
222+
registry.initializeSurface(1);
223+
EXPECT_TRUE(registry.getMap(1).second.empty());
224+
}
225+
226+
} // namespace facebook::react

‎scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7818,7 +7818,7 @@ struct facebook::react::PropsParserContext {
78187818
}
78197819

78207820
struct facebook::react::PropsSnapshot {
7821-
public facebook::react::BaseViewProps props;
7821+
public std::unique_ptr<facebook::react::BaseViewProps> props;
78227822
public std::unique_ptr<folly::dynamic> rawProps;
78237823
public std::unordered_set<facebook::react::PropName> propNames;
78247824
}

‎scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7578,7 +7578,7 @@ struct facebook::react::PropsParserContext {
75787578
}
75797579

75807580
struct facebook::react::PropsSnapshot {
7581-
public facebook::react::BaseViewProps props;
7581+
public std::unique_ptr<facebook::react::BaseViewProps> props;
75827582
public std::unique_ptr<folly::dynamic> rawProps;
75837583
public std::unordered_set<facebook::react::PropName> propNames;
75847584
}

‎scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7809,7 +7809,7 @@ struct facebook::react::PropsParserContext {
78097809
}
78107810

78117811
struct facebook::react::PropsSnapshot {
7812-
public facebook::react::BaseViewProps props;
7812+
public std::unique_ptr<facebook::react::BaseViewProps> props;
78137813
public std::unique_ptr<folly::dynamic> rawProps;
78147814
public std::unordered_set<facebook::react::PropName> propNames;
78157815
}

‎scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9805,7 +9805,7 @@ struct facebook::react::PropsParserContext {
98059805
}
98069806

98079807
struct facebook::react::PropsSnapshot {
9808-
public facebook::react::BaseViewProps props;
9808+
public std::unique_ptr<facebook::react::BaseViewProps> props;
98099809
public std::unique_ptr<folly::dynamic> rawProps;
98109810
public std::unordered_set<facebook::react::PropName> propNames;
98119811
}

‎scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9621,7 +9621,7 @@ struct facebook::react::PropsParserContext {
96219621
}
96229622

96239623
struct facebook::react::PropsSnapshot {
9624-
public facebook::react::BaseViewProps props;
9624+
public std::unique_ptr<facebook::react::BaseViewProps> props;
96259625
public std::unique_ptr<folly::dynamic> rawProps;
96269626
public std::unordered_set<facebook::react::PropName> propNames;
96279627
}

0 commit comments

Comments
 (0)