Shift fetching litho attributes to background thread

Summary: Due to litho component instances being immutable we are able to process them later if we hold on to the instance. We have added a Maybe deferred type which sort of resembles a Monad. It wraps a value which may or may not be calculated later.

Reviewed By: lblasa

Differential Revision: D41474251

fbshipit-source-id: 2ba6e688518dba55cf4aa5ba53f390a92cf0921f
This commit is contained in:
Luke De Feo
2022-11-23 03:45:26 -08:00
committed by Facebook GitHub Bot
parent c95c59342e
commit 7ec09b4f95
12 changed files with 109 additions and 45 deletions

View File

@@ -15,6 +15,8 @@ import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.LayoutPro
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Deferred
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import com.facebook.litho.DebugComponent
class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor<DebugComponent> {
@@ -67,8 +69,8 @@ class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescripto
private val UserPropsId =
MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Props")
override fun getData(node: DebugComponent): Map<MetadataId, InspectableObject> {
override fun getData(node: DebugComponent): MaybeDeferred<Map<MetadataId, InspectableObject>> {
return Deferred {
val attributeSections = mutableMapOf<MetadataId, InspectableObject>()
val layoutProps = LayoutPropExtractor.getProps(node)
@@ -79,7 +81,8 @@ class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescripto
attributeSections[UserPropsId] = InspectableObject(props.toMap())
}
return attributeSections
attributeSections
}
}
override fun getBounds(node: DebugComponent): Bounds =

View File

@@ -19,13 +19,14 @@ import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler
import com.facebook.flipper.plugins.uidebugger.traversal.PartialLayoutTraversal
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import kotlinx.serialization.json.Json
data class ScanResult(
val txId: Long,
val scanStart: Long,
val scanEnd: Long,
val nodes: List<Node>
val nodes: List<MaybeDeferred<Node>>
)
const val observerType = "FullScan"
@@ -62,6 +63,9 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
}
private fun sendSubtreeUpdate(input: ScanResult) {
val nodes = input.nodes.map { it.value() }
val deferredComputationComplete = System.currentTimeMillis()
val serialized =
Json.encodeToString(
SubtreeUpdateEvent.serializer(),
@@ -69,7 +73,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
input.txId,
observerType,
ApplicationRefDescriptor.getId(context.applicationRef),
input.nodes))
nodes))
val serializationEnd = System.currentTimeMillis()
context.connectionRef.connection?.send(
@@ -89,6 +93,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
input.scanStart,
input.scanEnd,
input.scanEnd,
deferredComputationComplete,
serializationEnd,
socketEnd,
input.nodes.size)))

View File

@@ -11,6 +11,8 @@ import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Immediate
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
/**
* A chained descriptor is a special type of descriptor that models the inheritance hierarchy in
@@ -81,7 +83,7 @@ abstract class ChainedDescriptor<T> : NodeDescriptor<T> {
open fun onGetChildren(node: T): List<Any>? = null
final override fun getData(node: T): Map<MetadataId, InspectableObject> {
final override fun getData(node: T): MaybeDeferred<Map<MetadataId, InspectableObject>> {
val builder = mutableMapOf<MetadataId, InspectableObject>()
onGetData(node, builder)
@@ -92,7 +94,7 @@ abstract class ChainedDescriptor<T> : NodeDescriptor<T> {
curDescriptor = curDescriptor.mSuper
}
return builder
return Immediate(builder)
}
/**

View File

@@ -11,6 +11,7 @@ import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
/*
Descriptors are an extension point used during traversal to extract data out of arbitrary
@@ -73,7 +74,7 @@ interface NodeDescriptor<T> {
* Get the data to show for this node in the sidebar of the inspector. The object will be shown in
* order and with a header matching the given name.
*/
fun getData(node: T): Map<MetadataId, InspectableObject>
fun getData(node: T): MaybeDeferred<Map<MetadataId, InspectableObject>>
/**
* Set of tags to describe this node in an abstract way for the UI Unfortunately this can't be an

View File

@@ -11,6 +11,7 @@ import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Immediate
object ObjectDescriptor : NodeDescriptor<Any> {
@@ -26,7 +27,7 @@ object ObjectDescriptor : NodeDescriptor<Any> {
override fun getChildren(node: Any) = listOf<Any>()
override fun getData(node: Any) = mutableMapOf<MetadataId, InspectableObject>()
override fun getData(node: Any) = Immediate(mapOf<MetadataId, InspectableObject>())
override fun getBounds(node: Any): Bounds = Bounds(0, 0, 0, 0)

View File

@@ -11,6 +11,7 @@ import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
/** a drawable or view that is mounted, along with the correct descriptor */
class OffsetChild(val child: Any, val descriptor: NodeDescriptor<Any>, val x: Int, val y: Int) {
@@ -37,7 +38,7 @@ object OffsetChildDescriptor : NodeDescriptor<OffsetChild> {
override fun getActiveChild(node: OffsetChild): Any? = node.descriptor.getActiveChild(node.child)
override fun getData(node: OffsetChild): Map<MetadataId, InspectableObject> =
override fun getData(node: OffsetChild): MaybeDeferred<Map<MetadataId, InspectableObject>> =
node.descriptor.getData(node.child)
override fun getTags(node: OffsetChild): Set<String> = node.descriptor.getTags(node.child)

View File

@@ -58,6 +58,7 @@ data class PerfStatsEvent(
val traversalComplete: Long,
val snapshotComplete: Long,
val queuingComplete: Long,
val deferredComputationComplete: Long,
val serializationComplete: Long,
val socketComplete: Long,
val nodesCount: Int

View File

@@ -23,6 +23,7 @@ import com.facebook.flipper.plugins.uidebugger.model.MetadataUpdateEvent
import com.facebook.flipper.plugins.uidebugger.model.Node
import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent
import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
@@ -37,7 +38,7 @@ data class CoordinateUpdate(val observerType: String, val nodeId: Id, val coordi
data class SubtreeUpdate(
val observerType: String,
val rootId: Id,
val nodes: List<Node>,
val deferredNodes: List<MaybeDeferred<Node>>,
val startTime: Long,
val traversalCompleteTime: Long,
val snapshotComplete: Long,
@@ -104,12 +105,13 @@ class TreeObserverManager(val context: Context) {
sendMetadata()
val serialized: String?
val nodes = treeUpdate.deferredNodes.map { it.value() }
val deferredComptationComplete = System.currentTimeMillis()
if (treeUpdate.snapshot == null) {
serialized =
Json.encodeToString(
SubtreeUpdateEvent.serializer(),
SubtreeUpdateEvent(
txId, treeUpdate.observerType, treeUpdate.rootId, treeUpdate.nodes))
SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.rootId, nodes))
} else {
val stream = ByteArrayOutputStream()
val base64Stream = Base64OutputStream(stream, Base64.DEFAULT)
@@ -118,8 +120,7 @@ class TreeObserverManager(val context: Context) {
serialized =
Json.encodeToString(
SubtreeUpdateEvent.serializer(),
SubtreeUpdateEvent(
txId, treeUpdate.observerType, treeUpdate.rootId, treeUpdate.nodes, snapshot))
SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.rootId, nodes, snapshot))
treeUpdate.snapshot.readyForReuse()
}
@@ -130,7 +131,7 @@ class TreeObserverManager(val context: Context) {
val socketEnd = System.currentTimeMillis()
Log.i(
LogTag,
"Sent event for ${treeUpdate.observerType} root ID ${treeUpdate.rootId} nodes ${treeUpdate.nodes.size}")
"Sent event for ${treeUpdate.observerType} root ID ${treeUpdate.rootId} nodes ${nodes.size}")
val perfStats =
PerfStatsEvent(
@@ -140,9 +141,10 @@ class TreeObserverManager(val context: Context) {
traversalComplete = treeUpdate.traversalCompleteTime,
snapshotComplete = treeUpdate.snapshotComplete,
queuingComplete = onWorkerThread,
deferredComputationComplete = deferredComptationComplete,
serializationComplete = serializationEnd,
socketComplete = socketEnd,
nodesCount = treeUpdate.nodes.size)
nodesCount = nodes.size)
context.connectionRef.connection?.send(
PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats))

View File

@@ -14,6 +14,8 @@ import com.facebook.flipper.plugins.uidebugger.descriptors.Id
import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
import com.facebook.flipper.plugins.uidebugger.model.Node
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
import com.facebook.flipper.plugins.uidebugger.util.Immediate
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
/**
* This will traverse the layout hierarchy until it sees a node that has an observer registered for
@@ -29,9 +31,9 @@ class PartialLayoutTraversal(
@Suppress("unchecked_cast")
internal fun NodeDescriptor<*>.asAny(): NodeDescriptor<Any> = this as NodeDescriptor<Any>
fun traverse(root: Any): Pair<List<Node>, List<Any>> {
fun traverse(root: Any): Pair<List<MaybeDeferred<Node>>, List<Any>> {
val visited = mutableListOf<Node>()
val visited = mutableListOf<MaybeDeferred<Node>>()
val observableRoots = mutableListOf<Any>()
val stack = mutableListOf<Any>()
@@ -53,6 +55,7 @@ class PartialLayoutTraversal(
if (shallow.contains(node)) {
visited.add(
Immediate(
Node(
descriptor.getId(node),
descriptor.getQualifiedName(node),
@@ -61,7 +64,7 @@ class PartialLayoutTraversal(
descriptor.getBounds(node),
emptySet(),
emptyList(),
null))
null)))
shallow.remove(node)
continue
@@ -93,15 +96,17 @@ class PartialLayoutTraversal(
val bounds = descriptor.getBounds(node)
val tags = descriptor.getTags(node)
visited.add(
attributes.map { attrs ->
Node(
descriptor.getId(node),
descriptor.getQualifiedName(node),
descriptor.getName(node),
attributes,
attrs,
bounds,
tags,
childrenIds,
activeChildId))
activeChildId)
})
} catch (exception: Exception) {
Log.e(LogTag, "Error while processing node ${node.javaClass.name} $node", exception)
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.flipper.plugins.uidebugger.util
/**
* This abstracts over a value which could be available immediately or calculated later. The use
* case is for shifting computation to a background thread.
*/
abstract class MaybeDeferred<T> {
abstract fun value(): T
/** similar to map on an Option or Functor in a functional language. */
abstract fun <U> map(fn: (T) -> U): MaybeDeferred<U>
}
class Immediate<T>(private val value: T) : MaybeDeferred<T>() {
override fun value(): T = value
override fun <U> map(fn: (T) -> U): MaybeDeferred<U> = Immediate(fn(value))
}
class Deferred<T>(private val lazyLoader: () -> T) : MaybeDeferred<T>() {
override fun value(): T = lazyLoader()
override fun <U> map(fn: (T) -> U): MaybeDeferred<U> {
return Deferred { fn(lazyLoader()) }
}
}

View File

@@ -63,11 +63,21 @@ const columns: DataTableColumn<PerfStatsEvent>[] = [
return formatDiff(row.snapshotComplete, row.queuingComplete);
},
},
{
key: 'deferredComputationComplete',
title: 'Deferred processing time',
onRender: (row: PerfStatsEvent) => {
return formatDiff(row.queuingComplete, row.deferredComputationComplete);
},
},
{
key: 'serializationComplete',
title: 'Serialization time',
onRender: (row: PerfStatsEvent) => {
return formatDiff(row.queuingComplete, row.serializationComplete);
return formatDiff(
row.deferredComputationComplete,
row.serializationComplete,
);
},
},
{

View File

@@ -40,8 +40,9 @@ export type PerfStatsEvent = {
start: number;
traversalComplete: number;
snapshotComplete: number;
serializationComplete: number;
queuingComplete: number;
deferredComputationComplete: number;
serializationComplete: number;
socketComplete: number;
nodesCount: number;
};