Skip to content

Commit c5d2e33

Browse files
Reduce animation registry copying and allocation (#58775)
Summary: Pull Request resolved: #58775 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] [Changed] - Reduce animation registry copying and allocation Differential Revision: D122570619
1 parent 0034e53 commit c5d2e33

14 files changed

Lines changed: 338 additions & 98 deletions

‎packages/react-native/Package.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ let reactFabric = RNTarget(
467467
path: "ReactCommon/react/renderer",
468468
excludedPaths: [
469469
"animated/tests",
470+
"animationbackend/tests",
470471
"animations/tests",
471472
"attributedstring/tests",
472473
"core/tests",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Pod::Spec.new do |s|
7878
ss.dependency "React-callinvoker"
7979
ss.dependency "React-timing"
8080
ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}")
81+
ss.exclude_files = "react/renderer/animationbackend/tests"
8182
ss.header_dir = "react/renderer/animationbackend"
8283
end
8384

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,27 @@ void AnimatedPropsRegistry::update(
3434
.first;
3535
}
3636
auto& snapshot = it->second;
37-
auto& viewProps = snapshot->props;
38-
3937
if (animatedProps.rawProps) {
4038
const auto& newRawProps = *animatedProps.rawProps;
4139
auto& currentRawProps = snapshot->rawProps;
4240

4341
if (currentRawProps) {
44-
auto newRawPropsDynamic = newRawProps.toDynamic();
45-
currentRawProps->merge_patch(newRawPropsDynamic);
42+
if (const auto* dynamic = newRawProps.getDynamic()) {
43+
currentRawProps->merge_patch(*dynamic);
44+
} else {
45+
currentRawProps->merge_patch(newRawProps.toDynamic());
46+
}
4647
} else {
4748
currentRawProps =
4849
std::make_unique<folly::dynamic>(newRawProps.toDynamic());
4950
}
5051
}
52+
if (!animatedProps.props.empty() && !snapshot->props) {
53+
snapshot->props = std::make_unique<BaseViewProps>();
54+
}
5155
for (const auto& animatedProp : animatedProps.props) {
5256
snapshot->propNames.insert(animatedProp->propName);
53-
cloneProp(viewProps, *animatedProp);
57+
cloneProp(*snapshot->props, *animatedProp);
5458
}
5559
}
5660
}
@@ -85,9 +89,14 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
8589
currentSnapshot->rawProps = std::move(propsSnapshot->rawProps);
8690
}
8791
}
88-
for (auto& propName : propsSnapshot->propNames) {
89-
currentSnapshot->propNames.insert(propName);
90-
updateProp(propName, currentSnapshot->props, *propsSnapshot);
92+
if (!currentSnapshot->props) {
93+
currentSnapshot->props = std::move(propsSnapshot->props);
94+
currentSnapshot->propNames = std::move(propsSnapshot->propNames);
95+
} else {
96+
for (auto& propName : propsSnapshot->propNames) {
97+
currentSnapshot->propNames.insert(propName);
98+
updateProp(propName, *currentSnapshot->props, *propsSnapshot);
99+
}
91100
}
92101
}
93102
}

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

Lines changed: 84 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(), 2u);
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
@@ -7817,7 +7817,7 @@ struct facebook::react::PropsParserContext {
78177817
}
78187818

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

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

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

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

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

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9807,7 +9807,7 @@ struct facebook::react::PropsParserContext {
98079807
}
98089808

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9623,7 +9623,7 @@ struct facebook::react::PropsParserContext {
96239623
}
96249624

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

0 commit comments

Comments
 (0)