Skip to content

Commit 089a101

Browse files
YevheniiKotyrlometa-codesync[bot]
authored andcommitted
Read only the drawn child's mix-blend-mode in Android drawChild (#59016)
Summary: `ReactViewGroup.drawChild` and `ReactRootView.drawChild` call `BlendModeHelper.needsIsolatedLayer(this)` before they read the drawn child's `mix_blend_mode` tag. `needsIsolatedLayer` reads that tag on every child of the group, and `dispatchDraw` calls `drawChild` once per child, so recording the display list of a group with N children reads N² tags. When no child sets a blend mode — the common case — `any` never short-circuits, so the whole scan runs for every child of every group that records. I found it profiling flings over a grid of a few hundred views on two release builds: sampling the main thread, `needsIsolatedLayer` under `drawChild` was 37% of display-list recording on a Snapdragon 845 (Android 10) and 46% on a Snapdragon 8 Gen 2 (Android 14). The guard adds nothing to the decision `drawChild` makes. A child that carries the tag makes `needsIsolatedLayer` true by definition, and a child without it draws with no layer whatever the guard says. So both overrides now read the drawn child's own tag. `draw()` keeps its group-wide check, which decides whether the whole group renders offscreen and runs once per recording. I searched the tracker for `needsIsolatedLayer`, `BlendModeHelper`, `mix_blend_mode` and `drawChild` and found no existing report or PR. ## Changelog: [ANDROID] [FIXED] - Drawing a view group no longer reads every sibling's mix-blend-mode for each child it draws Pull Request resolved: #59016 Test Plan: - `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.view.ReactViewGroupTest" -Preact.internal.useHermesStable=true`: on `main` the new `drawing a child reads no sibling's blend mode` test fails with `expected:<[3]> but was:<[21]>` — each child of a 20-child group has its tag read 21 times, against 3 in a 2-child group; with the fix both groups read each child equally often, and all 19 tests in the class pass. - The new `a child with a blend mode draws into a layer with that blend mode` test records `[null, MULTIPLY]` — the group's isolated layer, then the child's — before and after the change; with the child's layer skipped it fails with `Expecting actual: [null] to contain exactly (and in same order): [null, MULTIPLY]`. - `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true -Preact.internal.useHermesStable=true`: 707 tests, 0 failures. - `node ./scripts/format-kotlin.js --check` and `node ./scripts/format-java.js --check` pass. - The same change on 0.86.3, in an app with a method trace over those flings: `needsIsolatedLayer` ran 1,608 times for 1,400 `drawChild` calls before and 182 times for 3,234 after, every one of them from `draw()`. A `mixBlendMode: 'multiply'` view in the same app still blends. - Not run: the RNTester build and Android lint (`build_android`). Reviewed By: andrewdacenko Differential Revision: D124965507 Pulled By: javache fbshipit-source-id: e0155f6de592f9d14e8cb17a6b092b5fda711d04
1 parent 9b7c956 commit 089a101

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package com.facebook.react;
99

1010
import static com.facebook.infer.annotation.ThreadConfined.UI;
11-
import static com.facebook.react.uimanager.BlendModeHelper.needsIsolatedLayer;
1211
import static com.facebook.react.uimanager.common.UIManagerType.FABRIC;
1312
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT;
1413

@@ -308,7 +307,7 @@ protected void dispatchDraw(Canvas canvas) {
308307
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
309308

310309
BlendMode mixBlendMode = null;
311-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer(this)) {
310+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
312311
mixBlendMode = (BlendMode) child.getTag(R.id.mix_blend_mode);
313312
if (mixBlendMode != null) {
314313
Paint p = new Paint();

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
10031003
}
10041004

10051005
var mixBlendMode: BlendMode? = null
1006-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer(this)) {
1006+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
10071007
mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode
10081008
if (mixBlendMode != null) {
10091009
val p = Paint()

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/view/ReactViewGroupTest.kt‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ package com.facebook.react.views.view
99

1010
import android.app.Activity
1111
import android.content.Context
12+
import android.graphics.BlendMode
13+
import android.graphics.Canvas
14+
import android.graphics.Paint
1215
import android.view.View
1316
import android.view.ViewGroup
1417
import android.widget.FrameLayout
18+
import com.facebook.react.R
1519
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1620
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
1721
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
@@ -67,6 +71,32 @@ class ReactViewGroupTest {
6771
assertThat(rvg.childCount).isEqualTo(20)
6872
}
6973

74+
@Test
75+
fun `mix blend mode - drawing a child reads no sibling's blend mode`() {
76+
fun blendModeReadsPerChild(childCount: Int): Int {
77+
val rvg = ReactViewGroup(context)
78+
val children = List(childCount) { BlendModeReadCountingView(context) }
79+
children.forEach { rvg.addView(it) }
80+
rvg.layout(0, 0, 100, 100)
81+
rvg.draw(Canvas())
82+
return children.maxOf { it.blendModeReads }
83+
}
84+
85+
assertThat(blendModeReadsPerChild(20)).isEqualTo(blendModeReadsPerChild(2))
86+
}
87+
88+
@Test
89+
fun `mix blend mode - a child with a blend mode draws into a layer with that blend mode`() {
90+
val rvg = ReactViewGroup(context)
91+
rvg.addView(View(context))
92+
rvg.addView(View(context).apply { setTag(R.id.mix_blend_mode, BlendMode.MULTIPLY) })
93+
rvg.addView(View(context))
94+
rvg.layout(0, 0, 100, 100)
95+
val canvas = LayerRecordingCanvas()
96+
rvg.draw(canvas)
97+
assertThat(canvas.layerBlendModes).containsExactly(null, BlendMode.MULTIPLY)
98+
}
99+
70100
@Test
71101
fun `auto offscreen compositing - true when flag on, alpha lt 1, and elevated descendant`() {
72102
overrideAutoOffscreenFlag(true)
@@ -294,3 +324,23 @@ class TestView(context: Context, yPos: Int) : View(context) {
294324
class TestParent(context: Context) : ViewGroup(context) {
295325
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) = Unit
296326
}
327+
328+
private class BlendModeReadCountingView(context: Context) : View(context) {
329+
var blendModeReads = 0
330+
331+
override fun getTag(key: Int): Any? {
332+
if (key == R.id.mix_blend_mode) {
333+
blendModeReads++
334+
}
335+
return super.getTag(key)
336+
}
337+
}
338+
339+
private class LayerRecordingCanvas : Canvas() {
340+
val layerBlendModes = mutableListOf<BlendMode?>()
341+
342+
override fun saveLayer(left: Float, top: Float, right: Float, bottom: Float, paint: Paint?): Int {
343+
layerBlendModes.add(paint?.blendMode)
344+
return super.saveLayer(left, top, right, bottom, paint)
345+
}
346+
}

0 commit comments

Comments
 (0)