Skip to content

Commit 47df773

Browse files
Batch Android animated prop updates into one JNI call per surface
Summary: With the batched delegate from the previous PR, Android applied the backend's synchronous updates with one JNI call and one `ReadableNativeMap` per view per frame. When `optimizedAnimatedPropUpdates` is on, a surface's updates now go to the UI thread in a single call: `FabricMountingManager::synchronouslyUpdateAnimatedProps` encodes them into int and double buffers, and `BatchedAnimatedPropsMountItem` decodes and applies them. This continues the encoder and mount item from #56468. Views that are not mounted yet are still queued. A view is packed only when every prop it carries has a command. Unsupported props, null resets, platform colors, malformed values and typed props send the whole view through the existing `packAnimatedProps` serializer as a map instead. The commands cover the props Reanimated applies synchronously on Android, following the parity review in #56468 (comment). Angles are converted to radians in C++, and radii and translations keep their px or percent units. Because the decoder produces `JavaOnlyArray` transforms, `TransformHelper`'s Kotlin conversion now applies a uniform scale to Z as well as X and Y, matching the native conversion when scale is combined with 3D rotation. On a low-end Android phone (4× Cortex-A53) with 12 views animating opacity, UI-thread time per frame's synchronous batch dropped from about 6.5 ms to 3.4 ms. Changelog: [Android][Performance] - Batch synchronous animated prop updates when `optimizedAnimatedPropUpdates` is enabled Test Plan: - JVM: `BatchedAnimatedPropsMountItemTest`, `MountingManagerAnimatedPropsTest`, `MatrixMathHelperTest` and `SkewMatrixHelperTest` pass. The scale regression test fails without the `TransformHelper` change. - Native: the encoder, registry and `RawProps` tests pass on Android. The numeric-color fallback test fails without the checked conversion. - The Android arm64 release build succeeds. C++ API snapshots regenerated. Not built for iOS.
1 parent a7c3b7e commit 47df773

11 files changed

Lines changed: 772 additions & 0 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import com.facebook.react.fabric.mounting.MountingManager;
6363
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
6464
import com.facebook.react.fabric.mounting.mountitems.BatchMountItem;
65+
import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem;
6566
import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem;
6667
import com.facebook.react.fabric.mounting.mountitems.MountItem;
6768
import com.facebook.react.fabric.mounting.mountitems.MountItemFactory;
@@ -837,6 +838,27 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable
837838
ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber);
838839
}
839840

841+
/** Called from C++ via JNI with a batch encoded by AnimatedPropBufferEncoder.cpp. */
842+
@SuppressLint("NotInvokedPrivateMethod")
843+
@SuppressWarnings("unused")
844+
@UiThread
845+
@ThreadConfined(UI)
846+
private void synchronouslyUpdateViewBatch(
847+
int[] intBuffer, double[] doubleBuffer, Object[] rawPropsBuffer) {
848+
UiThreadUtil.assertOnUiThread();
849+
850+
int commitNumber = mCurrentSynchronousCommitNumber++;
851+
ReactMarker.logFabricMarker(
852+
ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_START, null, commitNumber);
853+
854+
new BatchedAnimatedPropsMountItem(
855+
intBuffer, doubleBuffer, rawPropsBuffer, mMountItemDispatcher)
856+
.execute(mMountingManager);
857+
858+
ReactMarker.logFabricMarker(
859+
ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber);
860+
}
861+
840862
/** Called from C++ via JNI. */
841863
@SuppressLint("NotInvokedPrivateMethod")
842864
@SuppressWarnings("unused")

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ internal class MountingManager(
268268
getSurfaceManagerForView(reactTag)?.storeSynchronousMountPropsOverride(reactTag, props)
269269
}
270270

271+
/** Returns false when no surface owns [reactTag] yet, so the caller can queue the update. */
272+
@UiThread
273+
fun updateAnimatedPropsSynchronously(reactTag: Int, props: ReadableMap): Boolean {
274+
assertOnUiThread()
275+
val surfaceMountingManager = getSurfaceManagerForView(reactTag) ?: return false
276+
surfaceMountingManager.storeSynchronousMountPropsOverride(reactTag, props)
277+
surfaceMountingManager.updatePropsSynchronously(reactTag, props)
278+
return true
279+
}
280+
271281
@UiThread
272282
fun updatePropsSynchronously(reactTag: Int, props: ReadableMap?) {
273283
assertOnUiThread()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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+
package com.facebook.react.fabric.mounting.mountitems
9+
10+
import android.view.View
11+
import com.facebook.react.bridge.JavaOnlyArray
12+
import com.facebook.react.bridge.JavaOnlyMap
13+
import com.facebook.react.bridge.ReadableMap
14+
import com.facebook.react.fabric.FabricUIManager.IS_DEVELOPMENT_ENVIRONMENT
15+
import com.facebook.react.fabric.mounting.MountItemDispatcher
16+
import com.facebook.react.fabric.mounting.MountingManager
17+
import com.facebook.react.uimanager.ViewProps
18+
19+
internal class BatchedAnimatedPropsMountItem(
20+
private val intBuffer: IntArray,
21+
private val doubleBuffer: DoubleArray,
22+
private val rawPropsBuffer: Array<Any?>,
23+
private val mountItemDispatcher: MountItemDispatcher,
24+
) : MountItem {
25+
26+
override fun execute(mountingManager: MountingManager) {
27+
decode { viewTag, props ->
28+
try {
29+
if (!mountingManager.updateAnimatedPropsSynchronously(viewTag, props)) {
30+
mountItemDispatcher.addMountItem(SynchronousMountItem(viewTag, props))
31+
}
32+
} catch (ex: Exception) {
33+
// Match SynchronousMountItem's handling of view-manager failures.
34+
}
35+
}
36+
}
37+
38+
internal fun decode(apply: (Int, ReadableMap) -> Unit) {
39+
var intIdx = 0
40+
var doubleIdx = 0
41+
var rawIdx = 0
42+
while (intIdx < intBuffer.size) {
43+
val command = intBuffer[intIdx++]
44+
require(command == CMD_START_OF_VIEW) { "Expected start of animated view, got $command" }
45+
val viewTag = intBuffer[intIdx++]
46+
47+
if (intBuffer[intIdx] == CMD_RAW_PROPS) {
48+
require(intBuffer[intIdx + 1] == CMD_END_OF_VIEW) { "Raw props must be the whole view" }
49+
intIdx += 2
50+
apply(viewTag, rawPropsBuffer[rawIdx++] as ReadableMap)
51+
continue
52+
}
53+
54+
val props = JavaOnlyMap()
55+
while (true) {
56+
val cmd = intBuffer[intIdx++]
57+
if (cmd == CMD_END_OF_VIEW) {
58+
break
59+
}
60+
61+
when (cmd) {
62+
CMD_OPACITY,
63+
CMD_ELEVATION,
64+
CMD_Z_INDEX,
65+
CMD_SHADOW_OPACITY,
66+
CMD_SHADOW_RADIUS,
67+
CMD_OUTLINE_OFFSET,
68+
CMD_OUTLINE_WIDTH -> props.putDouble(commandToString(cmd), doubleBuffer[doubleIdx++])
69+
CMD_BACKGROUND_COLOR,
70+
CMD_COLOR,
71+
CMD_TINT_COLOR,
72+
CMD_PLACEHOLDER_TEXT_COLOR,
73+
CMD_SHADOW_COLOR,
74+
CMD_BORDER_COLOR,
75+
CMD_BORDER_TOP_COLOR,
76+
CMD_BORDER_BOTTOM_COLOR,
77+
CMD_BORDER_LEFT_COLOR,
78+
CMD_BORDER_RIGHT_COLOR,
79+
CMD_BORDER_START_COLOR,
80+
CMD_BORDER_END_COLOR,
81+
CMD_BORDER_BLOCK_COLOR,
82+
CMD_BORDER_BLOCK_START_COLOR,
83+
CMD_BORDER_BLOCK_END_COLOR,
84+
CMD_OUTLINE_COLOR -> props.putInt(commandToString(cmd), intBuffer[intIdx++])
85+
CMD_BORDER_RADIUS,
86+
CMD_BORDER_TOP_LEFT_RADIUS,
87+
CMD_BORDER_TOP_RIGHT_RADIUS,
88+
CMD_BORDER_TOP_START_RADIUS,
89+
CMD_BORDER_TOP_END_RADIUS,
90+
CMD_BORDER_BOTTOM_LEFT_RADIUS,
91+
CMD_BORDER_BOTTOM_RIGHT_RADIUS,
92+
CMD_BORDER_BOTTOM_START_RADIUS,
93+
CMD_BORDER_BOTTOM_END_RADIUS,
94+
CMD_BORDER_START_START_RADIUS,
95+
CMD_BORDER_START_END_RADIUS,
96+
CMD_BORDER_END_START_RADIUS,
97+
CMD_BORDER_END_END_RADIUS ->
98+
putLength(
99+
props,
100+
commandToString(cmd),
101+
intBuffer[intIdx++],
102+
doubleBuffer[doubleIdx++],
103+
)
104+
CMD_START_OF_TRANSFORM -> {
105+
val transform = JavaOnlyArray()
106+
while (true) {
107+
val transformCmd = intBuffer[intIdx++]
108+
if (transformCmd == CMD_END_OF_TRANSFORM) {
109+
break
110+
}
111+
val name = transformCommandToString(transformCmd)
112+
val entry = JavaOnlyMap()
113+
when (transformCmd) {
114+
CMD_TRANSLATE_X,
115+
CMD_TRANSLATE_Y ->
116+
putLength(entry, name, intBuffer[intIdx++], doubleBuffer[doubleIdx++])
117+
CMD_MATRIX -> {
118+
val matrix = JavaOnlyArray()
119+
repeat(16) { matrix.pushDouble(doubleBuffer[doubleIdx++]) }
120+
entry.putArray(name, matrix)
121+
}
122+
// Angles are in radians.
123+
else -> entry.putDouble(name, doubleBuffer[doubleIdx++])
124+
}
125+
transform.pushMap(entry)
126+
}
127+
props.putArray(ViewProps.TRANSFORM, transform)
128+
}
129+
else -> error("Unknown animated prop command: $cmd")
130+
}
131+
}
132+
133+
apply(viewTag, props)
134+
}
135+
require(doubleIdx == doubleBuffer.size && rawIdx == rawPropsBuffer.size) {
136+
"Unused values in animated props buffer"
137+
}
138+
}
139+
140+
override fun toString(): String {
141+
if (!IS_DEVELOPMENT_ENVIRONMENT) return "BATCHED UPDATE PROPS <hidden>"
142+
val updates = mutableListOf<String>()
143+
decode { tag, props -> updates.add("[$tag]: ${props.toHashMap()}") }
144+
return "BATCHED UPDATE PROPS ${updates.joinToString()}"
145+
}
146+
147+
override fun getSurfaceId(): Int = View.NO_ID
148+
149+
companion object {
150+
// Keep command values in sync with AnimatedPropBufferEncoder.cpp.
151+
private const val CMD_START_OF_VIEW = 1
152+
private const val CMD_START_OF_TRANSFORM = 2
153+
private const val CMD_END_OF_TRANSFORM = 3
154+
private const val CMD_END_OF_VIEW = 4
155+
private const val CMD_RAW_PROPS = 5
156+
private const val CMD_OPACITY = 10
157+
private const val CMD_ELEVATION = 11
158+
private const val CMD_Z_INDEX = 12
159+
private const val CMD_SHADOW_OPACITY = 13
160+
private const val CMD_SHADOW_RADIUS = 14
161+
private const val CMD_BACKGROUND_COLOR = 15
162+
private const val CMD_COLOR = 16
163+
private const val CMD_TINT_COLOR = 17
164+
private const val CMD_PLACEHOLDER_TEXT_COLOR = 18
165+
private const val CMD_SHADOW_COLOR = 19
166+
private const val CMD_BORDER_RADIUS = 20
167+
private const val CMD_BORDER_TOP_LEFT_RADIUS = 21
168+
private const val CMD_BORDER_TOP_RIGHT_RADIUS = 22
169+
private const val CMD_BORDER_TOP_START_RADIUS = 23
170+
private const val CMD_BORDER_TOP_END_RADIUS = 24
171+
private const val CMD_BORDER_BOTTOM_LEFT_RADIUS = 25
172+
private const val CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26
173+
private const val CMD_BORDER_BOTTOM_START_RADIUS = 27
174+
private const val CMD_BORDER_BOTTOM_END_RADIUS = 28
175+
private const val CMD_BORDER_START_START_RADIUS = 29
176+
private const val CMD_BORDER_START_END_RADIUS = 30
177+
private const val CMD_BORDER_END_START_RADIUS = 31
178+
private const val CMD_BORDER_END_END_RADIUS = 32
179+
private const val CMD_BORDER_COLOR = 40
180+
private const val CMD_BORDER_TOP_COLOR = 41
181+
private const val CMD_BORDER_BOTTOM_COLOR = 42
182+
private const val CMD_BORDER_LEFT_COLOR = 43
183+
private const val CMD_BORDER_RIGHT_COLOR = 44
184+
private const val CMD_BORDER_START_COLOR = 45
185+
private const val CMD_BORDER_END_COLOR = 46
186+
private const val CMD_BORDER_BLOCK_COLOR = 47
187+
private const val CMD_BORDER_BLOCK_START_COLOR = 48
188+
private const val CMD_BORDER_BLOCK_END_COLOR = 49
189+
private const val CMD_OUTLINE_COLOR = 50
190+
private const val CMD_OUTLINE_OFFSET = 51
191+
private const val CMD_OUTLINE_WIDTH = 52
192+
private const val CMD_TRANSLATE_X = 100
193+
private const val CMD_TRANSLATE_Y = 101
194+
private const val CMD_SCALE = 102
195+
private const val CMD_SCALE_X = 103
196+
private const val CMD_SCALE_Y = 104
197+
private const val CMD_ROTATE = 105
198+
private const val CMD_ROTATE_X = 106
199+
private const val CMD_ROTATE_Y = 107
200+
private const val CMD_ROTATE_Z = 108
201+
private const val CMD_SKEW_X = 109
202+
private const val CMD_SKEW_Y = 110
203+
private const val CMD_MATRIX = 111
204+
private const val CMD_PERSPECTIVE = 112
205+
private const val CMD_UNIT_PX = 202
206+
private const val CMD_UNIT_PERCENT = 203
207+
208+
private fun putLength(map: JavaOnlyMap, name: String, unit: Int, value: Double) =
209+
when (unit) {
210+
CMD_UNIT_PX -> map.putDouble(name, value)
211+
CMD_UNIT_PERCENT -> map.putString(name, "$value%")
212+
else -> error("Unknown length unit: $unit")
213+
}
214+
215+
private fun commandToString(command: Int): String =
216+
when (command) {
217+
CMD_OPACITY -> ViewProps.OPACITY
218+
CMD_ELEVATION -> ViewProps.ELEVATION
219+
CMD_Z_INDEX -> ViewProps.Z_INDEX
220+
CMD_SHADOW_OPACITY -> "shadowOpacity"
221+
CMD_SHADOW_RADIUS -> "shadowRadius"
222+
CMD_BACKGROUND_COLOR -> ViewProps.BACKGROUND_COLOR
223+
CMD_COLOR -> ViewProps.COLOR
224+
CMD_TINT_COLOR -> "tintColor"
225+
CMD_PLACEHOLDER_TEXT_COLOR -> "placeholderTextColor"
226+
CMD_SHADOW_COLOR -> ViewProps.SHADOW_COLOR
227+
CMD_BORDER_RADIUS -> ViewProps.BORDER_RADIUS
228+
CMD_BORDER_TOP_LEFT_RADIUS -> ViewProps.BORDER_TOP_LEFT_RADIUS
229+
CMD_BORDER_TOP_RIGHT_RADIUS -> ViewProps.BORDER_TOP_RIGHT_RADIUS
230+
CMD_BORDER_TOP_START_RADIUS -> ViewProps.BORDER_TOP_START_RADIUS
231+
CMD_BORDER_TOP_END_RADIUS -> ViewProps.BORDER_TOP_END_RADIUS
232+
CMD_BORDER_BOTTOM_LEFT_RADIUS -> ViewProps.BORDER_BOTTOM_LEFT_RADIUS
233+
CMD_BORDER_BOTTOM_RIGHT_RADIUS -> ViewProps.BORDER_BOTTOM_RIGHT_RADIUS
234+
CMD_BORDER_BOTTOM_START_RADIUS -> ViewProps.BORDER_BOTTOM_START_RADIUS
235+
CMD_BORDER_BOTTOM_END_RADIUS -> ViewProps.BORDER_BOTTOM_END_RADIUS
236+
CMD_BORDER_START_START_RADIUS -> ViewProps.BORDER_START_START_RADIUS
237+
CMD_BORDER_START_END_RADIUS -> ViewProps.BORDER_START_END_RADIUS
238+
CMD_BORDER_END_START_RADIUS -> ViewProps.BORDER_END_START_RADIUS
239+
CMD_BORDER_END_END_RADIUS -> ViewProps.BORDER_END_END_RADIUS
240+
CMD_BORDER_COLOR -> ViewProps.BORDER_COLOR
241+
CMD_BORDER_TOP_COLOR -> ViewProps.BORDER_TOP_COLOR
242+
CMD_BORDER_BOTTOM_COLOR -> ViewProps.BORDER_BOTTOM_COLOR
243+
CMD_BORDER_LEFT_COLOR -> ViewProps.BORDER_LEFT_COLOR
244+
CMD_BORDER_RIGHT_COLOR -> ViewProps.BORDER_RIGHT_COLOR
245+
CMD_BORDER_START_COLOR -> ViewProps.BORDER_START_COLOR
246+
CMD_BORDER_END_COLOR -> ViewProps.BORDER_END_COLOR
247+
CMD_BORDER_BLOCK_COLOR -> ViewProps.BORDER_BLOCK_COLOR
248+
CMD_BORDER_BLOCK_START_COLOR -> ViewProps.BORDER_BLOCK_START_COLOR
249+
CMD_BORDER_BLOCK_END_COLOR -> ViewProps.BORDER_BLOCK_END_COLOR
250+
CMD_OUTLINE_COLOR -> ViewProps.OUTLINE_COLOR
251+
CMD_OUTLINE_OFFSET -> ViewProps.OUTLINE_OFFSET
252+
CMD_OUTLINE_WIDTH -> ViewProps.OUTLINE_WIDTH
253+
else -> error("Unknown animated prop command: $command")
254+
}
255+
256+
private fun transformCommandToString(command: Int): String =
257+
when (command) {
258+
CMD_TRANSLATE_X -> ViewProps.TRANSLATE_X
259+
CMD_TRANSLATE_Y -> ViewProps.TRANSLATE_Y
260+
CMD_SCALE -> "scale"
261+
CMD_SCALE_X -> ViewProps.SCALE_X
262+
CMD_SCALE_Y -> ViewProps.SCALE_Y
263+
CMD_ROTATE -> "rotate"
264+
CMD_ROTATE_X -> "rotateX"
265+
CMD_ROTATE_Y -> "rotateY"
266+
CMD_ROTATE_Z -> "rotateZ"
267+
CMD_SKEW_X -> "skewX"
268+
CMD_SKEW_Y -> "skewY"
269+
CMD_MATRIX -> "matrix"
270+
CMD_PERSPECTIVE -> "perspective"
271+
else -> error("Unknown animated prop command: $command")
272+
}
273+
}
274+
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TransformHelper.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public object TransformHelper {
141141
val scale = transform.getDouble(transformType)
142142
MatrixMathHelper.applyScaleX(helperMatrix, scale)
143143
MatrixMathHelper.applyScaleY(helperMatrix, scale)
144+
MatrixMathHelper.applyScaleZ(helperMatrix, scale)
144145
}
145146
"scaleX" -> MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType))
146147
"scaleY" -> MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType))

0 commit comments

Comments
 (0)