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.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId 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 import com.facebook.litho.DebugComponent
class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor<DebugComponent> { class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor<DebugComponent> {
@@ -67,19 +69,20 @@ class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescripto
private val UserPropsId = private val UserPropsId =
MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Props") 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 attributeSections = mutableMapOf<MetadataId, InspectableObject>() val layoutProps = LayoutPropExtractor.getProps(node)
attributeSections[LayoutId] = InspectableObject(layoutProps.toMap())
val layoutProps = LayoutPropExtractor.getProps(node) if (!node.canResolve()) {
attributeSections[LayoutId] = InspectableObject(layoutProps.toMap()) val props = ComponentPropExtractor.getProps(node.component)
attributeSections[UserPropsId] = InspectableObject(props.toMap())
}
if (!node.canResolve()) { attributeSections
val props = ComponentPropExtractor.getProps(node.component)
attributeSections[UserPropsId] = InspectableObject(props.toMap())
} }
return attributeSections
} }
override fun getBounds(node: DebugComponent): Bounds = 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.observers.TreeObserverFactory
import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler
import com.facebook.flipper.plugins.uidebugger.traversal.PartialLayoutTraversal import com.facebook.flipper.plugins.uidebugger.traversal.PartialLayoutTraversal
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
data class ScanResult( data class ScanResult(
val txId: Long, val txId: Long,
val scanStart: Long, val scanStart: Long,
val scanEnd: Long, val scanEnd: Long,
val nodes: List<Node> val nodes: List<MaybeDeferred<Node>>
) )
const val observerType = "FullScan" const val observerType = "FullScan"
@@ -62,6 +63,9 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
} }
private fun sendSubtreeUpdate(input: ScanResult) { private fun sendSubtreeUpdate(input: ScanResult) {
val nodes = input.nodes.map { it.value() }
val deferredComputationComplete = System.currentTimeMillis()
val serialized = val serialized =
Json.encodeToString( Json.encodeToString(
SubtreeUpdateEvent.serializer(), SubtreeUpdateEvent.serializer(),
@@ -69,7 +73,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
input.txId, input.txId,
observerType, observerType,
ApplicationRefDescriptor.getId(context.applicationRef), ApplicationRefDescriptor.getId(context.applicationRef),
input.nodes)) nodes))
val serializationEnd = System.currentTimeMillis() val serializationEnd = System.currentTimeMillis()
context.connectionRef.connection?.send( context.connectionRef.connection?.send(
@@ -89,6 +93,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
input.scanStart, input.scanStart,
input.scanEnd, input.scanEnd,
input.scanEnd, input.scanEnd,
deferredComputationComplete,
serializationEnd, serializationEnd,
socketEnd, socketEnd,
input.nodes.size))) 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.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId 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 * 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 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>() val builder = mutableMapOf<MetadataId, InspectableObject>()
onGetData(node, builder) onGetData(node, builder)
@@ -92,7 +94,7 @@ abstract class ChainedDescriptor<T> : NodeDescriptor<T> {
curDescriptor = curDescriptor.mSuper 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.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId 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 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 * 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. * 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 * 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.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Immediate
object ObjectDescriptor : NodeDescriptor<Any> { object ObjectDescriptor : NodeDescriptor<Any> {
@@ -26,7 +27,7 @@ object ObjectDescriptor : NodeDescriptor<Any> {
override fun getChildren(node: Any) = listOf<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) 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.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId 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 */ /** 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) { 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 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) node.descriptor.getData(node.child)
override fun getTags(node: OffsetChild): Set<String> = node.descriptor.getTags(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 traversalComplete: Long,
val snapshotComplete: Long, val snapshotComplete: Long,
val queuingComplete: Long, val queuingComplete: Long,
val deferredComputationComplete: Long,
val serializationComplete: Long, val serializationComplete: Long,
val socketComplete: Long, val socketComplete: Long,
val nodesCount: Int 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.Node
import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent
import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -37,7 +38,7 @@ data class CoordinateUpdate(val observerType: String, val nodeId: Id, val coordi
data class SubtreeUpdate( data class SubtreeUpdate(
val observerType: String, val observerType: String,
val rootId: Id, val rootId: Id,
val nodes: List<Node>, val deferredNodes: List<MaybeDeferred<Node>>,
val startTime: Long, val startTime: Long,
val traversalCompleteTime: Long, val traversalCompleteTime: Long,
val snapshotComplete: Long, val snapshotComplete: Long,
@@ -104,12 +105,13 @@ class TreeObserverManager(val context: Context) {
sendMetadata() sendMetadata()
val serialized: String? val serialized: String?
val nodes = treeUpdate.deferredNodes.map { it.value() }
val deferredComptationComplete = System.currentTimeMillis()
if (treeUpdate.snapshot == null) { if (treeUpdate.snapshot == null) {
serialized = serialized =
Json.encodeToString( Json.encodeToString(
SubtreeUpdateEvent.serializer(), SubtreeUpdateEvent.serializer(),
SubtreeUpdateEvent( SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.rootId, nodes))
txId, treeUpdate.observerType, treeUpdate.rootId, treeUpdate.nodes))
} else { } else {
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
val base64Stream = Base64OutputStream(stream, Base64.DEFAULT) val base64Stream = Base64OutputStream(stream, Base64.DEFAULT)
@@ -118,8 +120,7 @@ class TreeObserverManager(val context: Context) {
serialized = serialized =
Json.encodeToString( Json.encodeToString(
SubtreeUpdateEvent.serializer(), SubtreeUpdateEvent.serializer(),
SubtreeUpdateEvent( SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.rootId, nodes, snapshot))
txId, treeUpdate.observerType, treeUpdate.rootId, treeUpdate.nodes, snapshot))
treeUpdate.snapshot.readyForReuse() treeUpdate.snapshot.readyForReuse()
} }
@@ -130,7 +131,7 @@ class TreeObserverManager(val context: Context) {
val socketEnd = System.currentTimeMillis() val socketEnd = System.currentTimeMillis()
Log.i( Log.i(
LogTag, 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 = val perfStats =
PerfStatsEvent( PerfStatsEvent(
@@ -140,9 +141,10 @@ class TreeObserverManager(val context: Context) {
traversalComplete = treeUpdate.traversalCompleteTime, traversalComplete = treeUpdate.traversalCompleteTime,
snapshotComplete = treeUpdate.snapshotComplete, snapshotComplete = treeUpdate.snapshotComplete,
queuingComplete = onWorkerThread, queuingComplete = onWorkerThread,
deferredComputationComplete = deferredComptationComplete,
serializationComplete = serializationEnd, serializationComplete = serializationEnd,
socketComplete = socketEnd, socketComplete = socketEnd,
nodesCount = treeUpdate.nodes.size) nodesCount = nodes.size)
context.connectionRef.connection?.send( context.connectionRef.connection?.send(
PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats)) 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.descriptors.NodeDescriptor
import com.facebook.flipper.plugins.uidebugger.model.Node import com.facebook.flipper.plugins.uidebugger.model.Node
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory 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 * 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") @Suppress("unchecked_cast")
internal fun NodeDescriptor<*>.asAny(): NodeDescriptor<Any> = this as NodeDescriptor<Any> 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 observableRoots = mutableListOf<Any>()
val stack = mutableListOf<Any>() val stack = mutableListOf<Any>()
@@ -53,15 +55,16 @@ class PartialLayoutTraversal(
if (shallow.contains(node)) { if (shallow.contains(node)) {
visited.add( visited.add(
Node( Immediate(
descriptor.getId(node), Node(
descriptor.getQualifiedName(node), descriptor.getId(node),
descriptor.getName(node), descriptor.getQualifiedName(node),
emptyMap(), descriptor.getName(node),
descriptor.getBounds(node), emptyMap(),
emptySet(), descriptor.getBounds(node),
emptyList(), emptySet(),
null)) emptyList(),
null)))
shallow.remove(node) shallow.remove(node)
continue continue
@@ -93,15 +96,17 @@ class PartialLayoutTraversal(
val bounds = descriptor.getBounds(node) val bounds = descriptor.getBounds(node)
val tags = descriptor.getTags(node) val tags = descriptor.getTags(node)
visited.add( visited.add(
Node( attributes.map { attrs ->
descriptor.getId(node), Node(
descriptor.getQualifiedName(node), descriptor.getId(node),
descriptor.getName(node), descriptor.getQualifiedName(node),
attributes, descriptor.getName(node),
bounds, attrs,
tags, bounds,
childrenIds, tags,
activeChildId)) childrenIds,
activeChildId)
})
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(LogTag, "Error while processing node ${node.javaClass.name} $node", 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); return formatDiff(row.snapshotComplete, row.queuingComplete);
}, },
}, },
{
key: 'deferredComputationComplete',
title: 'Deferred processing time',
onRender: (row: PerfStatsEvent) => {
return formatDiff(row.queuingComplete, row.deferredComputationComplete);
},
},
{ {
key: 'serializationComplete', key: 'serializationComplete',
title: 'Serialization time', title: 'Serialization time',
onRender: (row: PerfStatsEvent) => { 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; start: number;
traversalComplete: number; traversalComplete: number;
snapshotComplete: number; snapshotComplete: number;
serializationComplete: number;
queuingComplete: number; queuingComplete: number;
deferredComputationComplete: number;
serializationComplete: number;
socketComplete: number; socketComplete: number;
nodesCount: number; nodesCount: number;
}; };