Skip to content

Commit 4f6243b

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
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 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: [Internal] Differential Revision: D122570619
1 parent 2d03488 commit 4f6243b

14 files changed

Lines changed: 328 additions & 97 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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,27 @@ void AnimatedPropsRegistry::update(
4646
.first;
4747
}
4848
auto& snapshot = it->second;
49-
auto& viewProps = snapshot->props;
50-
5149
if (animatedProps.rawProps) {
5250
const auto& newRawProps = *animatedProps.rawProps;
5351
auto& currentRawProps = snapshot->rawProps;
5452

5553
if (currentRawProps) {
56-
mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic());
54+
if (const auto* dynamic = newRawProps.getDynamic()) {
55+
mergeAnimatedRawProps(*currentRawProps, *dynamic);
56+
} else {
57+
mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic());
58+
}
5759
} else {
5860
currentRawProps =
5961
std::make_unique<folly::dynamic>(newRawProps.toDynamic());
6062
}
6163
}
64+
if (!animatedProps.props.empty() && !snapshot->props) {
65+
snapshot->props = std::make_unique<BaseViewProps>();
66+
}
6267
for (const auto& animatedProp : animatedProps.props) {
6368
snapshot->propNames.insert(animatedProp->propName);
64-
cloneProp(viewProps, *animatedProp);
69+
cloneProp(*snapshot->props, *animatedProp);
6570
}
6671
}
6772
}
@@ -97,9 +102,14 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
97102
currentSnapshot->rawProps = std::move(propsSnapshot->rawProps);
98103
}
99104
}
100-
for (auto& propName : propsSnapshot->propNames) {
101-
currentSnapshot->propNames.insert(propName);
102-
updateProp(propName, currentSnapshot->props, *propsSnapshot);
105+
if (!currentSnapshot->props) {
106+
currentSnapshot->props = std::move(propsSnapshot->props);
107+
currentSnapshot->propNames = std::move(propsSnapshot->propNames);
108+
} else {
109+
for (auto& propName : propsSnapshot->propNames) {
110+
currentSnapshot->propNames.insert(propName);
111+
updateProp(propName, *currentSnapshot->props, *propsSnapshot);
112+
}
103113
}
104114
}
105115
}

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

Lines changed: 86 additions & 81 deletions
Large diffs are not rendered by default.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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, mergesPendingUpdatesPerKey) {
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)));
91+
}
92+
93+
TEST_F(AnimatedPropsRegistryTest, mergesCommittedUpdatesPerKey) {
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)));
104+
}
105+
106+
TEST_F(AnimatedPropsRegistryTest, nullResetsArePreservedInPendingProps) {
107+
updateRaw(folly::dynamic::object("opacity", 0.25)("borderRadius", 4));
108+
updateRaw(folly::dynamic::object("opacity", nullptr));
109+
expectRawProps(folly::dynamic::object("opacity", nullptr)("borderRadius", 4));
110+
}
111+
112+
TEST_F(AnimatedPropsRegistryTest, nullResetsArePreservedInCommittedProps) {
113+
updateRaw(folly::dynamic::object("opacity", 0.25)("borderRadius", 4));
114+
snapshot();
115+
updateRaw(folly::dynamic::object("opacity", nullptr));
116+
expectRawProps(folly::dynamic::object("opacity", nullptr)("borderRadius", 4));
117+
updateRaw(folly::dynamic::object("opacity", 0.5));
118+
expectRawProps(folly::dynamic::object("opacity", 0.5)("borderRadius", 4));
119+
}
120+
121+
TEST_F(AnimatedPropsRegistryTest, arraysReplaceInsteadOfMerging) {
122+
updateRaw(
123+
folly::dynamic::object(
124+
"transform",
125+
folly::dynamic::array(
126+
folly::dynamic::object("translateX", 2),
127+
folly::dynamic::object("scale", 3))));
128+
snapshot();
129+
auto transform =
130+
folly::dynamic::array(folly::dynamic::object("rotate", "1rad"));
131+
updateRaw(folly::dynamic::object("transform", transform));
132+
EXPECT_EQ((*snapshot().rawProps)["transform"], transform);
133+
}
134+
135+
TEST_F(AnimatedPropsRegistryTest, valuesReplaceRegardlessOfType) {
136+
updateRaw(folly::dynamic::object("value", 5));
137+
snapshot();
138+
folly::dynamic object = folly::dynamic::object("width", 2)("height", nullptr);
139+
updateRaw(folly::dynamic::object("value", object));
140+
expectRawProps(folly::dynamic::object("value", object));
141+
updateRaw(folly::dynamic::object("value", 7));
142+
expectRawProps(folly::dynamic::object("value", 7));
143+
}
144+
145+
TEST_F(AnimatedPropsRegistryTest, typedPropsCanFollowRawProps) {
146+
updateRaw(folly::dynamic::object("opacity", 0.25));
147+
snapshot();
148+
AnimatedPropsBuilder builder;
149+
builder.setOpacity(0.75);
150+
update(builder.get());
151+
auto& props = snapshot();
152+
ASSERT_NE(props.props, nullptr);
153+
EXPECT_FLOAT_EQ(props.props->opacity, 0.75);
154+
EXPECT_TRUE(props.propNames.contains(OPACITY));
155+
EXPECT_EQ((*props.rawProps)["opacity"], 0.25);
156+
BaseViewProps viewProps;
157+
updateProp(OPACITY, viewProps, props);
158+
EXPECT_FLOAT_EQ(viewProps.opacity, 0.75);
159+
}
160+
161+
TEST_F(AnimatedPropsRegistryTest, rawPropsCanFollowTypedProps) {
162+
AnimatedPropsBuilder builder;
163+
builder.setOpacity(0.75);
164+
update(builder.get());
165+
snapshot();
166+
updateRaw(folly::dynamic::object("opacity", 0.25));
167+
auto& props = snapshot();
168+
ASSERT_NE(props.props, nullptr);
169+
EXPECT_FLOAT_EQ(props.props->opacity, 0.75);
170+
EXPECT_TRUE(props.propNames.contains(OPACITY));
171+
EXPECT_EQ((*props.rawProps)["opacity"], 0.25);
172+
}
173+
174+
TEST_F(AnimatedPropsRegistryTest, mergesTypedUpdatesAcrossCommits) {
175+
AnimatedPropsBuilder builder;
176+
builder.setOpacity(0.75);
177+
builder.setShadowRadius(2);
178+
update(builder.get());
179+
snapshot();
180+
builder.setOpacity(0.5);
181+
update(builder.get());
182+
auto& props = snapshot();
183+
ASSERT_NE(props.props, nullptr);
184+
EXPECT_FLOAT_EQ(props.props->opacity, 0.5);
185+
EXPECT_FLOAT_EQ(props.props->shadowRadius, 2);
186+
EXPECT_EQ(props.propNames.size(), 2u);
187+
}
188+
189+
TEST_F(AnimatedPropsRegistryTest, jsiRawPropsAreConverted) {
190+
auto runtime = hermes::makeHermesRuntime();
191+
{
192+
auto object = jsi::Object(*runtime);
193+
object.setProperty(*runtime, "opacity", 0.25);
194+
update(
195+
{{},
196+
std::make_unique<RawProps>(*runtime, jsi::Value(*runtime, object))});
197+
object.setProperty(*runtime, "opacity", 0.5);
198+
update(
199+
{{},
200+
std::make_unique<RawProps>(*runtime, jsi::Value(*runtime, object))});
201+
}
202+
runtime.reset();
203+
expectRawProps(folly::dynamic::object("opacity", 0.5));
204+
EXPECT_EQ(snapshot().props, nullptr);
205+
}
206+
207+
TEST_F(AnimatedPropsRegistryTest, stoppedSurfacesIgnoreUpdates) {
208+
registry.clearOnSurfaceStop(1);
209+
updateRaw(folly::dynamic::object("opacity", 0.5));
210+
registry.initializeSurface(1);
211+
EXPECT_TRUE(registry.getMap(1).second.empty());
212+
}
213+
214+
} // namespace facebook::react

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7816,7 +7816,7 @@ struct facebook::react::PropsParserContext {
78167816
}
78177817

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

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

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

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

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

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

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)