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:
committed by
Facebook GitHub Bot
parent
c76c993ce4
commit
9a270cdc7a
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,3 +49,5 @@ website/src/embedded-pages/docs/plugins/
|
||||
|
||||
# Logs
|
||||
**/*/flipper-server-log.out
|
||||
|
||||
*.salive
|
||||
|
||||
@@ -68,6 +68,7 @@ android {
|
||||
compileOnly deps.proguardAnnotations
|
||||
implementation deps.kotlinStdLibrary
|
||||
|
||||
implementation deps.kotlinCoroutinesAndroid
|
||||
implementation deps.openssl
|
||||
implementation deps.fbjni
|
||||
implementation deps.soloader
|
||||
|
||||
@@ -8,24 +8,34 @@
|
||||
package com.facebook.flipper.plugins.uidebugger
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.facebook.flipper.core.FlipperConnection
|
||||
import com.facebook.flipper.core.FlipperPlugin
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ConnectionRef
|
||||
import com.facebook.flipper.plugins.uidebugger.core.Context
|
||||
import com.facebook.flipper.plugins.uidebugger.core.NativeScanScheduler
|
||||
import com.facebook.flipper.plugins.uidebugger.core.*
|
||||
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val LogTag = "FlipperUIDebugger"
|
||||
const val LogTag = "FlipperUIDebugger"
|
||||
|
||||
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))
|
||||
|
||||
init {
|
||||
Log.i(LogTag, "Initializing UI Debugger")
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return "ui-debugger"
|
||||
}
|
||||
@@ -42,16 +52,19 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
|
||||
Json.encodeToString(
|
||||
InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef))))
|
||||
|
||||
nativeScanScheduler.start()
|
||||
context.treeObserverManager.start()
|
||||
// nativeScanScheduler.start()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun onDisconnect() {
|
||||
this.context.connectionRef.connection = null
|
||||
Log.e(LogTag, "disconnected")
|
||||
|
||||
this.nativeScanScheduler.stop()
|
||||
}
|
||||
|
||||
override fun runInBackground(): Boolean {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ open class EnumMapping<T>(val mapping: Map<String, T>) {
|
||||
if (entry != null) {
|
||||
return entry.key
|
||||
} else {
|
||||
Log.w(
|
||||
Log.d(
|
||||
LogTag,
|
||||
"Could not convert enum value ${enumValue.toString()} to string, known values ${mapping.entries}")
|
||||
return NoMapping
|
||||
|
||||
@@ -15,23 +15,19 @@ import java.lang.ref.WeakReference
|
||||
|
||||
class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks {
|
||||
interface ActivityStackChangedListener {
|
||||
fun onActivityAdded(activity: Activity, stack: List<Activity>)
|
||||
fun onActivityStackChanged(stack: List<Activity>)
|
||||
}
|
||||
|
||||
interface ActivityDestroyedListener {
|
||||
fun onActivityDestroyed(activity: Activity)
|
||||
fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
|
||||
}
|
||||
|
||||
private val rootsResolver: RootViewResolver
|
||||
private val activities: MutableList<WeakReference<Activity>>
|
||||
private var activityStackChangedlistener: ActivityStackChangedListener? = null
|
||||
private var activityDestroyedListener: ActivityDestroyedListener? = null
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activities.add(WeakReference<Activity>(activity))
|
||||
activityStackChangedlistener?.let { listener ->
|
||||
listener.onActivityStackChanged(this.activitiesStack)
|
||||
}
|
||||
activityStackChangedlistener?.onActivityAdded(activity, this.activitiesStack)
|
||||
activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
|
||||
}
|
||||
override fun onActivityStarted(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?.let { listener ->
|
||||
listener.onActivityStackChanged(this.activitiesStack)
|
||||
}
|
||||
activityStackChangedlistener?.onActivityDestroyed(activity, this.activitiesStack)
|
||||
activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
|
||||
}
|
||||
|
||||
fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
|
||||
activityStackChangedlistener = listener
|
||||
}
|
||||
|
||||
fun setActivityDestroyedListener(listener: ActivityDestroyedListener?) {
|
||||
activityDestroyedListener = listener
|
||||
}
|
||||
|
||||
val activitiesStack: List<Activity>
|
||||
get() {
|
||||
val stack: MutableList<Activity> = ArrayList<Activity>(activities.size)
|
||||
|
||||
@@ -8,12 +8,21 @@
|
||||
package com.facebook.flipper.plugins.uidebugger.core
|
||||
|
||||
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.observers.TreeObserverFactory
|
||||
|
||||
data class Context(
|
||||
val applicationRef: ApplicationRef,
|
||||
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?)
|
||||
|
||||
@@ -59,6 +59,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task<ScanResult> {
|
||||
result.txId,
|
||||
result.scanStart,
|
||||
result.scanEnd,
|
||||
result.scanEnd,
|
||||
serializationEnd,
|
||||
socketEnd,
|
||||
result.nodes.size)))
|
||||
|
||||
@@ -10,10 +10,8 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
|
||||
import android.app.Activity
|
||||
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
|
||||
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
|
||||
|
||||
class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
|
||||
val rootResolver = RootViewResolver()
|
||||
|
||||
override fun onInit() {}
|
||||
override fun onGetActiveChild(node: ApplicationRef): Any? {
|
||||
@@ -32,22 +30,8 @@ class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
|
||||
}
|
||||
|
||||
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) {
|
||||
if (activity.window.decorView == root.view) {
|
||||
children.add(activity)
|
||||
added = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
children.add(root.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
data class NativeScanEvent(val txId: Long, val nodes: List<Node>) {
|
||||
companion object {
|
||||
@@ -26,7 +33,8 @@ data class NativeScanEvent(val txId: Long, val nodes: List<Node>) {
|
||||
data class PerfStatsEvent(
|
||||
val txId: Long,
|
||||
val start: Long,
|
||||
val scanComplete: Long,
|
||||
val traversalComplete: Long,
|
||||
val queuingComplete: Long,
|
||||
val serializationComplete: Long,
|
||||
val socketComplete: Long,
|
||||
val nodesCount: Int
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ ext {
|
||||
ext.deps = [
|
||||
// Kotlin support
|
||||
kotlinStdLibrary : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION",
|
||||
kotlinCoroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4",
|
||||
// Android support
|
||||
supportAnnotations : "androidx.annotation:annotation:$ANDROIDX_VERSION",
|
||||
supportAppCompat : "androidx.appcompat:appcompat:$ANDROIDX_VERSION",
|
||||
|
||||
@@ -71,17 +71,24 @@ export const columns: DataTableColumn<PerfStatsEvent>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'scanComplete',
|
||||
title: 'Scan time',
|
||||
key: 'traversalComplete',
|
||||
title: 'Traversal time (Main thread)',
|
||||
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',
|
||||
title: 'Serialization time',
|
||||
onRender: (row: PerfStatsEvent) => {
|
||||
return formatDiff(row.scanComplete, row.serializationComplete);
|
||||
return formatDiff(row.queuingComplete, row.serializationComplete);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,8 +13,9 @@ import {Id, UINode} from './types';
|
||||
export type PerfStatsEvent = {
|
||||
txId: number;
|
||||
start: number;
|
||||
scanComplete: number;
|
||||
traversalComplete: number;
|
||||
serializationComplete: number;
|
||||
queuingComplete: number;
|
||||
socketComplete: number;
|
||||
nodesCount: number;
|
||||
};
|
||||
@@ -22,6 +23,7 @@ export type PerfStatsEvent = {
|
||||
type Events = {
|
||||
init: {rootId: string};
|
||||
nativeScan: {txId: number; nodes: UINode[]};
|
||||
subtreeUpdate: {txId: number; nodes: UINode[]};
|
||||
perfStats: PerfStatsEvent;
|
||||
};
|
||||
|
||||
@@ -38,9 +40,17 @@ export function plugin(client: PluginClient<Events>) {
|
||||
});
|
||||
|
||||
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}) => {
|
||||
//Native scan is a full update so overwrite everything
|
||||
nodesAtom.set(new Map(nodes.map((node) => [node.id, node])));
|
||||
console.log(nodesAtom.get());
|
||||
});
|
||||
|
||||
return {rootId, nodes: nodesAtom, perfEvents};
|
||||
|
||||
Reference in New Issue
Block a user