Skip to content

Commit f1a8173

Browse files
kingsleyadiofacebook-github-bot
authored andcommitted
Track state reads in Draw phase
Summary: With the necessary plugs and buttons now in place. Tracking state reads in Draw phase can finally happen. All state reads in the Draw phase will be committed into the UiStateReadRecords (similar to binders). These shall be used later to optimize the drawing phase by efficiently determining which draw hosts need to be invalidated Reviewed By: adityasharat Differential Revision: D77378261 fbshipit-source-id: 8aa53e57f751869ca1e5ba156d66190dab62257f
1 parent 88a4dd4 commit f1a8173

File tree

9 files changed

+124
-23
lines changed

9 files changed

+124
-23
lines changed

litho-core/src/main/java/com/facebook/litho/BaseMountingView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ constructor(context: ComponentContext, attrs: AttributeSet? = null) :
136136

137137
private var onDirtyMountListener: OnDirtyMountListener? = null
138138

139-
private val uiStateReadRecords = UiStateReadRecords(this)
139+
@get:JvmName("getUiStateReadRecords") internal val uiStateReadRecords = UiStateReadRecords(this)
140140

141141
private val mountState = MountState(this, systrace, uiStateReadRecords)
142142

litho-core/src/main/java/com/facebook/litho/ComponentRenderer.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package com.facebook.litho
2020

2121
import com.facebook.litho.state.StateReadRecorder
22+
import com.facebook.litho.state.UiStateReadRecords
2223

2324
/**
2425
* Wrapper around the Component-specific render implementation.
@@ -49,3 +50,9 @@ internal val ComponentContext.isReadTrackingEnabled: Boolean
4950
// Workaround for package-private calls directly in an inline function that seem
5051
// to generate bytecode that's incompatible with Java call-sites
5152
get() = lithoTree?.isReadTrackingEnabled == true
53+
54+
internal val ComponentContext.uiStateReadRecords: UiStateReadRecords
55+
get() =
56+
checkNotNull(lithoTree?.uiStateReadRecordsProvider?.getUiStateReadRecords()) {
57+
"Could not retrieve the UI state read records. This is likely because the LithoTree is not initialized."
58+
}

litho-core/src/main/java/com/facebook/litho/ComponentTree.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
import com.facebook.litho.perfboost.LithoPerfBooster;
8080
import com.facebook.litho.state.StateId;
8181
import com.facebook.litho.state.TreeStateProvider;
82+
import com.facebook.litho.state.UiStateReadRecords;
83+
import com.facebook.litho.state.UiStateReadRecordsProvider;
8284
import com.facebook.litho.stats.LithoStats;
8385
import com.facebook.rendercore.LogLevel;
8486
import com.facebook.rendercore.MountContentPools;
@@ -114,6 +116,7 @@ public class ComponentTree
114116
implements LithoVisibilityEventsListener,
115117
StateUpdater,
116118
TreeStateProvider,
119+
UiStateReadRecordsProvider,
117120
MountedViewReference,
118121
ErrorComponentReceiver,
119122
LithoTreeLifecycleProvider {
@@ -2580,6 +2583,15 @@ private void debugLog(String eventName, String info) {
25802583
}
25812584
}
25822585

2586+
@Override
2587+
public UiStateReadRecords getUiStateReadRecords() {
2588+
if (mLithoView == null) {
2589+
throw new IllegalStateException(
2590+
"Trying to get UI state read records without a set host view");
2591+
}
2592+
return mLithoView.getUiStateReadRecords();
2593+
}
2594+
25832595
private static void bindHandlesToComponentTree(
25842596
ComponentTree componentTree, LayoutState layoutState) {
25852597
for (Handle handle : layoutState.getComponentHandles()) {

litho-core/src/main/java/com/facebook/litho/LithoRenderer.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ class LithoRenderer(
6060
stateUpdater = this,
6161
rootHost = this,
6262
errorComponentReceiver = this,
63-
treeLifecycle = NoOpLifecycleProvider())
63+
treeLifecycle = NoOpLifecycleProvider(),
64+
uiStateReadRecordsProvider = {
65+
val host =
66+
rootHost as? BaseMountingView
67+
?: error("Trying to get UI state read records without a set host")
68+
host.uiStateReadRecords
69+
})
6470
private val errorComponentRef = AtomicReference<Component?>(null)
6571

6672
/** The root component that this renderer is responsible for rendering. */

litho-core/src/main/java/com/facebook/litho/LithoTree.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.facebook.infer.annotation.ThreadConfined
2020
import com.facebook.litho.config.ComponentsConfiguration
2121
import com.facebook.litho.state.StateProviderImpl
2222
import com.facebook.litho.state.TreeStateProvider
23+
import com.facebook.litho.state.UiStateReadRecordsProvider
2324
import java.util.concurrent.atomic.AtomicInteger
2425
import java.util.concurrent.atomic.AtomicReference
2526

@@ -37,7 +38,8 @@ internal constructor(
3738
@field:ThreadConfined(ThreadConfined.ANY) val stateUpdater: StateUpdater,
3839
@field:ThreadConfined(ThreadConfined.UI) val rootHost: RootHostProvider,
3940
val errorComponentReceiver: ErrorComponentReceiver,
40-
val treeLifecycle: LithoRendererLifecycleProvider
41+
val treeLifecycle: LithoRendererLifecycleProvider,
42+
internal val uiStateReadRecordsProvider: UiStateReadRecordsProvider,
4143
) {
4244

4345
val isReadTrackingEnabled: Boolean =
@@ -61,7 +63,8 @@ internal constructor(
6163
stateUpdater = stateUpdater,
6264
rootHost = componentTree,
6365
errorComponentReceiver = componentTree,
64-
treeLifecycle = componentTree)
66+
treeLifecycle = componentTree,
67+
uiStateReadRecordsProvider = componentTree)
6568

6669
@JvmStatic
6770
fun generateComponentTreeId(): Int {

litho-core/src/main/java/com/facebook/litho/PrimitiveComponentScope.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,14 @@ fun PrimitiveComponentScope.useNestedTree(
278278
stateUpdater = stateUpdater,
279279
rootHost = nestedTreeState.mountedViewReference,
280280
errorComponentReceiver = { errorComponentRef.update(AtomicReference(it)) },
281-
treeLifecycle = nestedTreeState.treeLifecycleProvider)
281+
treeLifecycle = nestedTreeState.treeLifecycleProvider,
282+
uiStateReadRecordsProvider = {
283+
val host = nestedTreeState.mountedViewReference.mountedView
284+
check(host is BaseMountingView) {
285+
"Trying to get UI state read records without a set host"
286+
}
287+
host.uiStateReadRecords
288+
})
282289
}
283290
val componentContext =
284291
ComponentContext(

litho-core/src/main/java/com/facebook/litho/graphics/drawscope/DrawingStyles.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,36 @@ package com.facebook.litho.graphics.drawscope
1919
import androidx.core.graphics.withSave
2020
import com.facebook.litho.ComponentHost
2121
import com.facebook.litho.Style
22+
import com.facebook.litho.binders.ModelWithContext
2223
import com.facebook.litho.binders.onBindHostView
24+
import com.facebook.litho.uiStateReadRecords
2325
import com.facebook.rendercore.graphics.drawscope.CanvasDrawScope
2426
import com.facebook.rendercore.graphics.drawscope.DrawScope
2527

2628
fun Style.drawBehind(block: DrawScope.() -> Unit): Style {
2729
return this +
2830
onBindHostView(Unit) { content ->
31+
val model = binderModel as ModelWithContext
32+
val uiStateReadsRecords = model.scopedContext.uiStateReadRecords
33+
// Retrieve the binderId during binder execution so that we can use it later in Draw
34+
val binderId = binderId
35+
2936
content as ComponentHost
3037
content.drawBehind = { canvas ->
31-
canvas.withSave {
32-
CanvasDrawScope(
33-
// canvas = canvas,
34-
// context = draw context,
35-
// size = Size(w, h),
36-
)
37-
.block()
38+
uiStateReadsRecords.recordOnDraw(binderId.renderUnitId) {
39+
canvas.withSave {
40+
CanvasDrawScope(
41+
// canvas = canvas,
42+
// context = draw context,
43+
// size = Size(w, h),
44+
)
45+
.block()
46+
}
3847
}
3948
}
40-
onUnbind { content.drawBehind = null }
49+
onUnbind {
50+
content.drawBehind = null
51+
uiStateReadsRecords.removeDrawScope(binderId.renderUnitId)
52+
}
4153
}
4254
}

litho-core/src/main/java/com/facebook/litho/state/UiStateReadRecords.kt

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package com.facebook.litho.state
1818

1919
import androidx.annotation.MainThread
20+
import androidx.collection.MutableLongSet
2021
import androidx.collection.MutableScatterSet
2122
import androidx.collection.ScatterSet
23+
import androidx.collection.mutableLongSetOf
2224
import androidx.collection.mutableScatterMapOf
2325
import androidx.collection.mutableScatterSetOf
2426
import com.facebook.rendercore.BinderId
@@ -27,16 +29,19 @@ import com.facebook.rendercore.BinderObserver
2729
/**
2830
* A [BinderObserver] responsible for tracking and recording state reads in binders.
2931
*
30-
* The [UiStateReadRecords] keeps a live [records] of which state is read and by who. This may be
31-
* used to determine which binders need to be re-bound when a state that they read is updated, which
32-
* can help to significantly optimize binder execution in the mount phase.
32+
* The [UiStateReadRecords] keeps a live record of which state is read and by who. These records are
33+
* exclusively those that are read during the UI phases (namely Mount and Draw). These records may
34+
* be used to determine which binders or draw hosts need to be re-bound or invalidated respectively,
35+
* when a state that they read is updated, which can help to significantly optimize execution in the
36+
* respective phases.
3337
*
3438
* @param config Supplies the read tracking status and current treeId to the observer.
3539
*/
3640
@MainThread
3741
internal class UiStateReadRecords(private val config: Config) : BinderObserver() {
3842

39-
private val records = mutableScatterMapOf<StateId, MutableScatterSet<BinderId>>()
43+
private val onBindRecords = mutableScatterMapOf<StateId, MutableScatterSet<BinderId>>()
44+
private val onDrawRecords = mutableScatterMapOf<StateId, MutableLongSet>()
4045

4146
override fun onBind(binderId: BinderId, func: () -> Unit) {
4247
if (!config.isReadTrackingEnabled) return func()
@@ -45,7 +50,7 @@ internal class UiStateReadRecords(private val config: Config) : BinderObserver()
4550
val reads = StateReadRecorder.record(treeId, func)
4651
synchronized(this) {
4752
reads.forEach { stateId ->
48-
val binderIds = records.getOrPut(stateId) { mutableScatterSetOf() }
53+
val binderIds = onBindRecords.getOrPut(stateId) { mutableScatterSetOf() }
4954
binderIds.add(binderId)
5055
}
5156
}
@@ -57,25 +62,49 @@ internal class UiStateReadRecords(private val config: Config) : BinderObserver()
5762

5863
synchronized(this) {
5964
val stateIdsToRemove = mutableSetOf<StateId>()
60-
records.forEach { stateId, binderIds ->
65+
onBindRecords.forEach { stateId, binderIds ->
6166
val removed = binderIds.remove(binderId)
6267
if (removed && binderIds.isEmpty()) stateIdsToRemove.add(stateId)
6368
}
64-
stateIdsToRemove.forEach { records.remove(it) }
69+
stateIdsToRemove.forEach { onBindRecords.remove(it) }
70+
}
71+
}
72+
73+
fun recordOnDraw(hostId: Long, func: () -> Unit) {
74+
if (!config.isReadTrackingEnabled) return func()
75+
76+
val treeId = config.currentTreeId()
77+
val reads = StateReadRecorder.record(treeId, func)
78+
reads.forEach { stateId ->
79+
val hostIds = onDrawRecords.getOrPut(stateId) { mutableLongSetOf() }
80+
hostIds.add(hostId)
81+
}
82+
}
83+
84+
fun removeDrawScope(hostId: Long) {
85+
if (!config.isReadTrackingEnabled) return
86+
87+
synchronized(this) {
88+
val stateIdsToRemove = mutableSetOf<StateId>()
89+
onDrawRecords.forEach { stateId, hostIds ->
90+
val removed = hostIds.remove(hostId)
91+
if (removed && hostIds.isEmpty()) stateIdsToRemove.add(stateId)
92+
}
93+
stateIdsToRemove.forEach { onDrawRecords.remove(it) }
6594
}
6695
}
6796

6897
fun hasBindersForState(dirtyStates: Set<StateId>): Boolean {
6998
return synchronized(this) {
70-
dirtyStates.any { stateId -> records[stateId]?.isNotEmpty() == true }
99+
dirtyStates.any { stateId -> onBindRecords[stateId]?.isNotEmpty() == true }
71100
}
72101
}
73102

74-
fun takeSnapshotForState(dirtyStates: Set<StateId>): ScatterSet<BinderId> {
103+
fun takeBinderSnapshotForState(dirtyStates: Set<StateId>): ScatterSet<BinderId> {
75104
val result = mutableScatterSetOf<BinderId>()
76105
synchronized(this) {
77106
dirtyStates.forEach { stateId ->
78-
val binderIds = records[stateId]
107+
val binderIds = onBindRecords[stateId]
79108
if (binderIds != null) result.addAll(binderIds)
80109
}
81110
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.facebook.litho.state
18+
19+
/**
20+
* Internal abstraction used to provide access to the [UiStateReadRecords] directly from the
21+
* [LithoTree].
22+
*/
23+
internal fun interface UiStateReadRecordsProvider {
24+
fun getUiStateReadRecords(): UiStateReadRecords
25+
}

0 commit comments

Comments
 (0)