Tree observer

Summary:
Added concept of a tree observer which is responsible for listening to the changes for a portion of the UI tree. This structure nests so Tree observers can hold child tree observers which emit events on a different cadence. This structure should allow us to incorporate different UI frameworks down the road as well as native android views.

We push the tree updates from the tree observers onto a channel and setup a coroutine to consume this channel, serialize and send down the wire.

Reviewed By: lblasa

Differential Revision: D39276681

fbshipit-source-id: a4bc23b3578a8a10b57dd11fe88b273e1ce09ad8
This commit is contained in:
Luke De Feo
2022-09-12 03:48:43 -07:00
committed by Facebook GitHub Bot
parent c76c993ce4
commit 9a270cdc7a
16 changed files with 480 additions and 53 deletions

2
.gitignore vendored
View File

@@ -49,3 +49,5 @@ website/src/embedded-pages/docs/plugins/
# Logs # Logs
**/*/flipper-server-log.out **/*/flipper-server-log.out
*.salive

View File

@@ -68,6 +68,7 @@ android {
compileOnly deps.proguardAnnotations compileOnly deps.proguardAnnotations
implementation deps.kotlinStdLibrary implementation deps.kotlinStdLibrary
implementation deps.kotlinCoroutinesAndroid
implementation deps.openssl implementation deps.openssl
implementation deps.fbjni implementation deps.fbjni
implementation deps.soloader implementation deps.soloader

View File

@@ -8,24 +8,34 @@
package com.facebook.flipper.plugins.uidebugger package com.facebook.flipper.plugins.uidebugger
import android.app.Application import android.app.Application
import android.util.Log
import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.core.FlipperPlugin import com.facebook.flipper.core.FlipperPlugin
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef import com.facebook.flipper.plugins.uidebugger.core.*
import com.facebook.flipper.plugins.uidebugger.core.ConnectionRef import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.core.NativeScanScheduler
import com.facebook.flipper.plugins.uidebugger.model.InitEvent import com.facebook.flipper.plugins.uidebugger.model.InitEvent
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 kotlinx.coroutines.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
val LogTag = "FlipperUIDebugger" const val LogTag = "FlipperUIDebugger"
class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin { class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
private val context: Context = Context(ApplicationRef(application), ConnectionRef(null)) private val context: Context =
Context(
ApplicationRef(application),
ConnectionRef(null),
DescriptorRegister.withDefaults(),
TreeObserverFactory.withDefaults())
private val nativeScanScheduler = Scheduler(NativeScanScheduler(context)) private val nativeScanScheduler = Scheduler(NativeScanScheduler(context))
init {
Log.i(LogTag, "Initializing UI Debugger")
}
override fun getId(): String { override fun getId(): String {
return "ui-debugger" return "ui-debugger"
} }
@@ -42,16 +52,19 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
Json.encodeToString( Json.encodeToString(
InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef)))) InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef))))
nativeScanScheduler.start() context.treeObserverManager.start()
// nativeScanScheduler.start()
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun onDisconnect() { override fun onDisconnect() {
this.context.connectionRef.connection = null this.context.connectionRef.connection = null
Log.e(LogTag, "disconnected")
this.nativeScanScheduler.stop() this.nativeScanScheduler.stop()
} }
override fun runInBackground(): Boolean { override fun runInBackground(): Boolean {
return false return true
} }
} }

View File

@@ -18,7 +18,7 @@ open class EnumMapping<T>(val mapping: Map<String, T>) {
if (entry != null) { if (entry != null) {
return entry.key return entry.key
} else { } else {
Log.w( Log.d(
LogTag, LogTag,
"Could not convert enum value ${enumValue.toString()} to string, known values ${mapping.entries}") "Could not convert enum value ${enumValue.toString()} to string, known values ${mapping.entries}")
return NoMapping return NoMapping

View File

@@ -15,23 +15,19 @@ import java.lang.ref.WeakReference
class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks { class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks {
interface ActivityStackChangedListener { interface ActivityStackChangedListener {
fun onActivityAdded(activity: Activity, stack: List<Activity>)
fun onActivityStackChanged(stack: List<Activity>) fun onActivityStackChanged(stack: List<Activity>)
} fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
interface ActivityDestroyedListener {
fun onActivityDestroyed(activity: Activity)
} }
private val rootsResolver: RootViewResolver private val rootsResolver: RootViewResolver
private val activities: MutableList<WeakReference<Activity>> private val activities: MutableList<WeakReference<Activity>>
private var activityStackChangedlistener: ActivityStackChangedListener? = null private var activityStackChangedlistener: ActivityStackChangedListener? = null
private var activityDestroyedListener: ActivityDestroyedListener? = null
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities.add(WeakReference<Activity>(activity)) activities.add(WeakReference<Activity>(activity))
activityStackChangedlistener?.let { listener -> activityStackChangedlistener?.onActivityAdded(activity, this.activitiesStack)
listener.onActivityStackChanged(this.activitiesStack) activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
}
} }
override fun onActivityStarted(activity: Activity) {} override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {} override fun onActivityResumed(activity: Activity) {}
@@ -47,21 +43,14 @@ class ApplicationRef(val application: Application) : Application.ActivityLifecyc
} }
} }
activityDestroyedListener?.let { listener -> listener.onActivityDestroyed(activity) } activityStackChangedlistener?.onActivityDestroyed(activity, this.activitiesStack)
activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
activityStackChangedlistener?.let { listener ->
listener.onActivityStackChanged(this.activitiesStack)
}
} }
fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) { fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
activityStackChangedlistener = listener activityStackChangedlistener = listener
} }
fun setActivityDestroyedListener(listener: ActivityDestroyedListener?) {
activityDestroyedListener = listener
}
val activitiesStack: List<Activity> val activitiesStack: List<Activity>
get() { get() {
val stack: MutableList<Activity> = ArrayList<Activity>(activities.size) val stack: MutableList<Activity> = ArrayList<Activity>(activities.size)

View File

@@ -8,12 +8,21 @@
package com.facebook.flipper.plugins.uidebugger.core package com.facebook.flipper.plugins.uidebugger.core
import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.plugins.uidebugger.PartialLayoutTraversal
import com.facebook.flipper.plugins.uidebugger.TreeObserverManager
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
data class Context( data class Context(
val applicationRef: ApplicationRef, val applicationRef: ApplicationRef,
val connectionRef: ConnectionRef, val connectionRef: ConnectionRef,
val descriptorRegister: DescriptorRegister = DescriptorRegister.withDefaults() val descriptorRegister: DescriptorRegister,
) val observerFactory: TreeObserverFactory,
) {
val layoutTraversal: PartialLayoutTraversal =
PartialLayoutTraversal(descriptorRegister, observerFactory)
val treeObserverManager = TreeObserverManager(this)
}
data class ConnectionRef(var connection: FlipperConnection?) data class ConnectionRef(var connection: FlipperConnection?)

View File

@@ -59,6 +59,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
result.txId, result.txId,
result.scanStart, result.scanStart,
result.scanEnd, result.scanEnd,
result.scanEnd,
serializationEnd, serializationEnd,
socketEnd, socketEnd,
result.nodes.size))) result.nodes.size)))

View File

@@ -10,10 +10,8 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
import android.app.Activity import android.app.Activity
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() { class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
val rootResolver = RootViewResolver()
override fun onInit() {} override fun onInit() {}
override fun onGetActiveChild(node: ApplicationRef): Any? { override fun onGetActiveChild(node: ApplicationRef): Any? {
@@ -32,22 +30,8 @@ class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
} }
override fun onGetChildren(applicationRef: ApplicationRef, children: MutableList<Any>) { override fun onGetChildren(applicationRef: ApplicationRef, children: MutableList<Any>) {
val activeRoots = rootResolver.listActiveRootViews()
activeRoots?.let { roots ->
for (root: RootViewResolver.RootView in roots) {
var added = false
for (activity: Activity in applicationRef.activitiesStack) { for (activity: Activity in applicationRef.activitiesStack) {
if (activity.window.decorView == root.view) {
children.add(activity) children.add(activity)
added = true
break
}
}
if (!added) {
children.add(root.view)
}
}
} }
} }

View File

@@ -14,6 +14,13 @@ data class InitEvent(val rootId: String) {
} }
} }
@kotlinx.serialization.Serializable
data class SubtreeUpdateEvent(val txId: Long, val observerType: String, val nodes: List<Node>) {
companion object {
const val name = "subtreeUpdate"
}
}
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class NativeScanEvent(val txId: Long, val nodes: List<Node>) { data class NativeScanEvent(val txId: Long, val nodes: List<Node>) {
companion object { companion object {
@@ -26,7 +33,8 @@ data class NativeScanEvent(val txId: Long, val nodes: List<Node>) {
data class PerfStatsEvent( data class PerfStatsEvent(
val txId: Long, val txId: Long,
val start: Long, val start: Long,
val scanComplete: Long, val traversalComplete: Long,
val queuingComplete: Long,
val serializationComplete: Long, val serializationComplete: Long,
val socketComplete: Long, val socketComplete: Long,
val nodesCount: Int val nodesCount: Int

View File

@@ -0,0 +1,93 @@
/*
* 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.observers
import android.app.Activity
import android.content.ContextWrapper
import android.util.Log
import android.view.View
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.SubtreeUpdate
import com.facebook.flipper.plugins.uidebugger.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.identityHashCode
/**
* responsible for observing the activity stack and managing the subscription to the top most
* content view (decor view)
*/
class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRef>() {
override fun subscribe(node: Any) {
Log.i(LogTag, "subscribing to application / activity changes")
val applicationRef = node as ApplicationRef
val addRemoveListener =
object : ApplicationRef.ActivityStackChangedListener {
override fun onActivityAdded(activity: Activity, stack: List<Activity>) {
val start = System.currentTimeMillis()
val (nodes, skipped) = context.layoutTraversal.traverse(applicationRef)
val observer =
context.observerFactory.createObserver(activity.window.decorView, context)!!
observer.subscribe(activity.window.decorView)
children[activity.window.decorView.identityHashCode()] = observer
context.treeObserverManager.emit(
SubtreeUpdate("Application", nodes, start, System.currentTimeMillis()))
Log.i(
LogTag,
"Activity added,stack size ${stack.size} found ${nodes.size} skipped $skipped Listeners $children")
}
override fun onActivityStackChanged(stack: List<Activity>) {}
override fun onActivityDestroyed(activity: Activity, stack: List<Activity>) {
val start = System.currentTimeMillis()
val (nodes, skipped) = context.layoutTraversal.traverse(applicationRef)
val observer = children[activity.window.decorView.identityHashCode()]
children.remove(activity.window.decorView.identityHashCode())
observer?.cleanUpRecursive()
context.treeObserverManager.emit(
SubtreeUpdate("Application", nodes, start, System.currentTimeMillis()))
Log.i(
LogTag,
"Activity removed,stack size ${stack.size} found ${nodes.size} skipped $skipped Listeners $children")
}
}
context.applicationRef.setActivityStackChangedListener(addRemoveListener)
Log.i(LogTag, "${context.applicationRef.rootViews.size} root views")
Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities")
val stack = context.applicationRef.activitiesStack
for (activity in stack) {
addRemoveListener.onActivityAdded(activity, stack)
}
}
private fun getActivity(view: View): Activity? {
var context: android.content.Context? = view.context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = context.baseContext
}
return null
}
override fun unsubscribe() {
context.applicationRef.setActivityStackChangedListener(null)
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.observers
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.SubtreeUpdate
import com.facebook.flipper.plugins.uidebugger.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.Context
typealias DecorView = View
/** Responsible for subscribing to updates to the content view of an activity */
class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
val throttleTimeMs = 500
// maybe should be weak reference in ctor?
private var nodeRef: View? = null
private var listener: ViewTreeObserver.OnPreDrawListener? = null
override fun subscribe(node: Any) {
node as View
nodeRef = node
Log.i(LogTag, "Subscribing to decor view changes")
listener =
object : ViewTreeObserver.OnPreDrawListener {
var lastSend = 0L
override fun onPreDraw(): Boolean {
val start = System.currentTimeMillis()
if (start - lastSend > throttleTimeMs) {
val (nodes, skipped) = context.layoutTraversal.traverse(node)
val traversalComplete = System.currentTimeMillis()
context.treeObserverManager.emit(
SubtreeUpdate("DecorView", nodes, start, traversalComplete))
lastSend = System.currentTimeMillis()
}
return true
}
}
node.viewTreeObserver.addOnPreDrawListener(listener)
}
override fun unsubscribe() {
Log.i(LogTag, "Try Unsubscribing to decor view changes")
listener.let {
Log.i(LogTag, "Actually Unsubscribing to decor view changes")
nodeRef?.viewTreeObserver?.removeOnPreDrawListener(it)
listener = null
nodeRef = null
}
}
}
object DecorViewTreeObserverBuilder : TreeObserverBuilder<DecorView> {
override fun canBuildFor(node: Any): Boolean {
return node.javaClass.simpleName.contains("DecorView")
}
override fun build(context: Context): TreeObserver<DecorView> {
Log.i(LogTag, "Building decor view observer")
return DecorViewObserver(context)
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.observers
import com.facebook.flipper.plugins.uidebugger.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.Context
interface TreeObserverBuilder<T> {
fun canBuildFor(node: Any): Boolean
fun build(context: Context): TreeObserver<T>
}
class TreeObserverFactory() {
private val builders = mutableListOf<TreeObserverBuilder<*>>()
fun <T> register(builder: TreeObserverBuilder<T>) {
builders.add(builder)
}
fun hasObserverFor(node: Any): Boolean {
return builders.any { it.canBuildFor(node) }
}
fun createObserver(node: Any, context: Context): TreeObserver<*>? {
return builders.find { it.canBuildFor(node) }?.build(context)
}
companion object {
fun withDefaults(): TreeObserverFactory {
val factory = TreeObserverFactory()
factory.register(DecorViewTreeObserverBuilder)
return factory
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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
import android.util.Log
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.descriptors.Descriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
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.observers.ApplicationTreeObserver
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json
data class SubtreeUpdate(
val observerType: String,
val nodes: List<Node>,
val startTime: Long,
val traversalCompleteTime: Long
)
/** Holds the instances of Tree observers */
class TreeObserverManager(val context: Context) {
private val rootObserver = ApplicationTreeObserver(context)
private val treeUpdates = Channel<SubtreeUpdate>(Channel.UNLIMITED)
private val workerScope = CoroutineScope(Dispatchers.IO)
private val txId = AtomicInteger()
fun emit(update: SubtreeUpdate) {
treeUpdates.trySend(update)
}
/**
* 1. Sets up the root observer
* 2. Starts worker to listen to channel, which serializers and sends data over connection
*/
fun start() {
rootObserver.subscribe(context.applicationRef)
workerScope.launch {
while (isActive) {
try {
val observation = treeUpdates.receive()
val onWorkerThread = System.currentTimeMillis()
val txId = txId.getAndIncrement().toLong()
val serialized =
Json.encodeToString(
SubtreeUpdateEvent.serializer(),
SubtreeUpdateEvent(txId, observation.observerType, observation.nodes))
val serializationEnd = System.currentTimeMillis()
context.connectionRef.connection?.send(SubtreeUpdateEvent.name, serialized)
val socketEnd = System.currentTimeMillis()
Log.i(
LogTag, "Sent event for ${observation.observerType} nodes ${observation.nodes.size}")
val perfStats =
PerfStatsEvent(
txId = txId,
start = observation.startTime,
traversalComplete = observation.traversalCompleteTime,
queuingComplete = onWorkerThread,
serializationComplete = serializationEnd,
socketComplete = socketEnd,
nodesCount = observation.nodes.size)
context.connectionRef.connection?.send(
PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats))
} catch (e: java.lang.Exception) {
Log.e(LogTag, "Error in channel ", e)
}
}
Log.i(LogTag, "shutting down worker")
}
}
fun stop() {
rootObserver.cleanUpRecursive()
treeUpdates.close()
workerScope.cancel()
}
}
/*
Stateful class that manages some subtree in the UI Hierarchy.
It is responsible for:
1. listening to the relevant framework events
2. Traversing the hierarchy of the managed nodes
3. Diffing to previous state (optional)
4. Pushing out updates for its entire set of nodes
If while traversing it encounters a node type which has its own TreeObserver, it
does not traverse that, instead it sets up a Tree observer responsible for that subtree
The parent is responsible for detecting when a child observer needs to be cleaned up
*/
abstract class TreeObserver<T> {
protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf()
// todo try to pass T again?
abstract fun subscribe(node: Any)
abstract fun unsubscribe()
fun cleanUpRecursive() {
children.values.forEach { it.cleanUpRecursive() }
unsubscribe()
children.clear()
}
}
typealias HashCode = Int
fun Any.identityHashCode(): HashCode {
return System.identityHashCode(this)
}
class PartialLayoutTraversal(
private val descriptorRegister: DescriptorRegister,
private val treeObserverfactory: TreeObserverFactory,
) {
internal fun Descriptor<*>.asAny(): Descriptor<Any> = this as Descriptor<Any>
fun traverse(root: Any): Pair<MutableList<Node>, List<Any>> {
val visited = mutableListOf<Node>()
val skipped = mutableListOf<Any>()
val stack = mutableListOf<Any>()
stack.add(root)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
try {
// if we encounter a node that has it own observer, dont traverse
if (node != root && treeObserverfactory.hasObserverFor(node)) {
skipped.add(node)
continue
}
val descriptor = descriptorRegister.descriptorForClassUnsafe(node::class.java).asAny()
val children = mutableListOf<Any>()
descriptor.getChildren(node, children)
val childrenIds = mutableListOf<String>()
for (child in children) {
// it might make sense one day to remove id from the descriptor since its always the
// hash code
val childDescriptor =
descriptorRegister.descriptorForClassUnsafe(child::class.java).asAny()
childrenIds.add(childDescriptor.getId(child))
stack.add(child)
}
val attributes = mutableMapOf<String, InspectableObject>()
descriptor.getData(node, attributes)
// NOTE active child null here
visited.add(
Node(descriptor.getId(node), descriptor.getName(node), attributes, childrenIds, null))
} catch (exception: Exception) {
Log.e(LogTag, "Error while processing node ${node.javaClass.name} ${node} ", exception)
}
}
return Pair(visited, skipped)
}
}

View File

@@ -55,6 +55,7 @@ ext {
ext.deps = [ ext.deps = [
// Kotlin support // Kotlin support
kotlinStdLibrary : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION", kotlinStdLibrary : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION",
kotlinCoroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4",
// Android support // Android support
supportAnnotations : "androidx.annotation:annotation:$ANDROIDX_VERSION", supportAnnotations : "androidx.annotation:annotation:$ANDROIDX_VERSION",
supportAppCompat : "androidx.appcompat:appcompat:$ANDROIDX_VERSION", supportAppCompat : "androidx.appcompat:appcompat:$ANDROIDX_VERSION",

View File

@@ -71,17 +71,24 @@ export const columns: DataTableColumn<PerfStatsEvent>[] = [
}, },
}, },
{ {
key: 'scanComplete', key: 'traversalComplete',
title: 'Scan time', title: 'Traversal time (Main thread)',
onRender: (row: PerfStatsEvent) => { onRender: (row: PerfStatsEvent) => {
return formatDiff(row.start, row.scanComplete); return formatDiff(row.start, row.traversalComplete);
},
},
{
key: 'queuingComplete',
title: 'Queuing time',
onRender: (row: PerfStatsEvent) => {
return formatDiff(row.traversalComplete, row.queuingComplete);
}, },
}, },
{ {
key: 'serializationComplete', key: 'serializationComplete',
title: 'Serialization time', title: 'Serialization time',
onRender: (row: PerfStatsEvent) => { onRender: (row: PerfStatsEvent) => {
return formatDiff(row.scanComplete, row.serializationComplete); return formatDiff(row.queuingComplete, row.serializationComplete);
}, },
}, },
{ {

View File

@@ -13,8 +13,9 @@ import {Id, UINode} from './types';
export type PerfStatsEvent = { export type PerfStatsEvent = {
txId: number; txId: number;
start: number; start: number;
scanComplete: number; traversalComplete: number;
serializationComplete: number; serializationComplete: number;
queuingComplete: number;
socketComplete: number; socketComplete: number;
nodesCount: number; nodesCount: number;
}; };
@@ -22,6 +23,7 @@ export type PerfStatsEvent = {
type Events = { type Events = {
init: {rootId: string}; init: {rootId: string};
nativeScan: {txId: number; nodes: UINode[]}; nativeScan: {txId: number; nodes: UINode[]};
subtreeUpdate: {txId: number; nodes: UINode[]};
perfStats: PerfStatsEvent; perfStats: PerfStatsEvent;
}; };
@@ -38,9 +40,17 @@ export function plugin(client: PluginClient<Events>) {
}); });
const nodesAtom = createState<Map<Id, UINode>>(new Map()); const nodesAtom = createState<Map<Id, UINode>>(new Map());
client.onMessage('subtreeUpdate', ({nodes}) => {
nodesAtom.update((draft) => {
for (const node of nodes) {
draft.set(node.id, node);
}
});
});
client.onMessage('nativeScan', ({nodes}) => { client.onMessage('nativeScan', ({nodes}) => {
//Native scan is a full update so overwrite everything
nodesAtom.set(new Map(nodes.map((node) => [node.id, node]))); nodesAtom.set(new Map(nodes.map((node) => [node.id, node])));
console.log(nodesAtom.get());
}); });
return {rootId, nodes: nodesAtom, perfEvents}; return {rootId, nodes: nodesAtom, perfEvents};