From 9a270cdc7a35fed5aa4cf201c932cf37b8e608ad Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Mon, 12 Sep 2022 03:48:43 -0700 Subject: [PATCH] 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 --- .gitignore | 2 + android/build.gradle | 1 + .../uidebugger/UIDebuggerFlipperPlugin.kt | 29 ++- .../plugins/uidebugger/common/EnumMapping.kt | 2 +- .../plugins/uidebugger/core/ApplicationRef.kt | 23 +-- .../plugins/uidebugger/core/Context.kt | 13 +- .../uidebugger/core/NativeScanScheduler.kt | 1 + .../descriptors/ApplicationRefDescriptor.kt | 20 +- .../plugins/uidebugger/model/Events.kt | 10 +- .../observers/ApplicationTreeObserver.kt | 93 +++++++++ .../observers/DecorViewTreeObserver.kt | 77 +++++++ .../uidebugger/observers/ObserverFactory.kt | 43 ++++ .../uidebugger/observers/TreeObserver.kt | 189 ++++++++++++++++++ build.gradle | 1 + .../public/ui-debugger/components/main.tsx | 15 +- desktop/plugins/public/ui-debugger/index.tsx | 14 +- 16 files changed, 480 insertions(+), 53 deletions(-) create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/DecorViewTreeObserver.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ObserverFactory.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt diff --git a/.gitignore b/.gitignore index 961268360..2429b3ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ website/src/embedded-pages/docs/plugins/ # Logs **/*/flipper-server-log.out + +*.salive diff --git a/android/build.gradle b/android/build.gradle index 6f789dcbe..fcdab512a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -68,6 +68,7 @@ android { compileOnly deps.proguardAnnotations implementation deps.kotlinStdLibrary + implementation deps.kotlinCoroutinesAndroid implementation deps.openssl implementation deps.fbjni implementation deps.soloader diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt index ecdd2417e..7334bb458 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt @@ -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 } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt index dbea0b0c9..64f2eb657 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt @@ -18,7 +18,7 @@ open class EnumMapping(val mapping: Map) { 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 diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt index 04d66daf8..d3f09fd41 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt @@ -15,23 +15,19 @@ import java.lang.ref.WeakReference class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks { interface ActivityStackChangedListener { + fun onActivityAdded(activity: Activity, stack: List) fun onActivityStackChanged(stack: List) - } - - interface ActivityDestroyedListener { - fun onActivityDestroyed(activity: Activity) + fun onActivityDestroyed(activity: Activity, stack: List) } private val rootsResolver: RootViewResolver private val activities: MutableList> private var activityStackChangedlistener: ActivityStackChangedListener? = null - private var activityDestroyedListener: ActivityDestroyedListener? = null override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { activities.add(WeakReference(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 get() { val stack: MutableList = ArrayList(activities.size) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt index fb795e4df..c74269116 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt @@ -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?) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt index 0406d2803..eb71128d5 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt @@ -59,6 +59,7 @@ class NativeScanScheduler(val context: Context) : Scheduler.Task { result.txId, result.scanStart, result.scanEnd, + result.scanEnd, serializationEnd, socketEnd, result.nodes.size))) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt index 19d6e4b68..d76b8fbf7 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt @@ -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() { - val rootResolver = RootViewResolver() override fun onInit() {} override fun onGetActiveChild(node: ApplicationRef): Any? { @@ -32,22 +30,8 @@ class ApplicationRefDescriptor : AbstractChainedDescriptor() { } override fun onGetChildren(applicationRef: ApplicationRef, children: MutableList) { - 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) - } - } + for (activity: Activity in applicationRef.activitiesStack) { + children.add(activity) } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt index 873a7ca8f..54b91acef 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt @@ -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) { + companion object { + const val name = "subtreeUpdate" + } +} + @kotlinx.serialization.Serializable data class NativeScanEvent(val txId: Long, val nodes: List) { companion object { @@ -26,7 +33,8 @@ data class NativeScanEvent(val txId: Long, val nodes: List) { 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 diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt new file mode 100644 index 000000000..4374a2156 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt @@ -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() { + + 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) { + 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) {} + + override fun onActivityDestroyed(activity: Activity, stack: List) { + 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) + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/DecorViewTreeObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/DecorViewTreeObserver.kt new file mode 100644 index 000000000..cae214627 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/DecorViewTreeObserver.kt @@ -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() { + + 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 { + override fun canBuildFor(node: Any): Boolean { + return node.javaClass.simpleName.contains("DecorView") + } + + override fun build(context: Context): TreeObserver { + Log.i(LogTag, "Building decor view observer") + return DecorViewObserver(context) + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ObserverFactory.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ObserverFactory.kt new file mode 100644 index 000000000..9fd757423 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ObserverFactory.kt @@ -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 { + + fun canBuildFor(node: Any): Boolean + fun build(context: Context): TreeObserver +} + +class TreeObserverFactory() { + + private val builders = mutableListOf>() + + fun register(builder: TreeObserverBuilder) { + 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 + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt new file mode 100644 index 000000000..88ad2b3a8 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt @@ -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, + 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(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 { + + protected val children: MutableMap> = 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 = this as Descriptor + + fun traverse(root: Any): Pair, List> { + + val visited = mutableListOf() + val skipped = mutableListOf() + val stack = mutableListOf() + 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() + descriptor.getChildren(node, children) + + val childrenIds = mutableListOf() + 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() + 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) + } +} diff --git a/build.gradle b/build.gradle index 9c929ffb8..77e5107eb 100644 --- a/build.gradle +++ b/build.gradle @@ -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", diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index f98f43c54..f4dcd23b9 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -71,17 +71,24 @@ export const columns: DataTableColumn[] = [ }, }, { - 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); }, }, { diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 9f030384d..c104332c2 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -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) { }); const nodesAtom = createState>(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};