From 433061d3777e0aa2f9e9fa07d355bf63e39ec1ae Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Tue, 27 Sep 2022 13:00:04 -0700 Subject: [PATCH] Take snapshot on subtree update Summary: DecorView owns a BitmapPool to take snapshots of the view. These snapshots are later on serialised by the manager. There's a couple of unrelated changes in this diff but that were already in place making it hard to split. (1) Renamed 'traverseAndSend' to 'processUpdate'. Why? The observers as a whole shouldn't necessary know that their 'observation' is being sent to any place. Future changes should move the send logic altogether from the observer too. But that can be made within the scope of a different diff. (2) There was a bug for nodes that were being observed but then unsubscribed from. If the nodes were being marked for observation and observer was already into place, these were not being told to subscribe again for changes. Reviewed By: LukeDefeo Differential Revision: D39812943 fbshipit-source-id: af98c5caf54e1c69f97043bae95049395a2e4545 --- .../plugins/uidebugger/litho/LithoObserver.kt | 4 +- .../plugins/uidebugger/model/Events.kt | 8 ++- .../observers/ApplicationTreeObserver.kt | 7 +- .../observers/DecorViewTreeObserver.kt | 18 +++-- .../uidebugger/observers/TreeObserver.kt | 67 +++++++++++++------ .../observers/TreeObserverManager.kt | 48 ++++++++++--- 6 files changed, 106 insertions(+), 46 deletions(-) diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt index 26515e017..b3b9fb837 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt @@ -19,7 +19,7 @@ class LithoViewTreeObserver(val context: Context) : TreeObserver() { override val type = "Litho" - var nodeRef: LithoView? = null + private var nodeRef: LithoView? = null override fun subscribe(node: Any) { @@ -27,7 +27,7 @@ class LithoViewTreeObserver(val context: Context) : TreeObserver() { nodeRef = node as LithoView - val listener: (view: LithoView) -> Unit = { traverseAndSend(context, node) } + val listener: (view: LithoView) -> Unit = { processUpdate(context, node) } node.setOnDirtyMountListener(listener) listener(node) 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 0fc3fd6a0..6b1b08120 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 @@ -17,7 +17,13 @@ data class InitEvent(val rootId: Id) { } @kotlinx.serialization.Serializable -data class SubtreeUpdateEvent(val txId: Long, val observerType: String, val nodes: List) { +data class SubtreeUpdateEvent( + val txId: Long, + val observerType: String, + val rootId: Id, + val nodes: List, + val snapshot: String? = null +) { companion object { const val name = "subtreeUpdate" } 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 index 95773f6bb..2a719f5f9 100644 --- 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 @@ -35,18 +35,13 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver) { Log.i(LogTag, "Root views updated, num ${rootViews.size}") - traverseAndSend(context, applicationRef) + processUpdate(context, applicationRef) } } context.applicationRef.rootsResolver.attachListener(rootViewListener) // On subscribe, trigger a traversal on whatever roots we have rootViewListener.onRootViewsChanged(applicationRef.rootViews) - // TODO: Subscribing to root view changes but not to activity changes. - // Obviously changes in activities have an effect on root views, but - // then it may make sense to unsubscribe the root views listener instead - // of activities. - Log.i(LogTag, "${context.applicationRef.rootViews.size} root views") Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities") } 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 index 3e64d9caa..b85d1e6d6 100644 --- 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 @@ -11,6 +11,7 @@ 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.common.BitmapPool import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest import java.lang.ref.WeakReference @@ -37,14 +38,17 @@ class DecorViewObserver(val context: Context) : TreeObserver() { Log.i(LogTag, "Subscribing to decor view changes") - val throttleSend = + val throttledUpdate = throttleLatest?>(throttleTimeMs, waitScope, mainScope) { weakView -> - weakView?.get()?.let { view -> traverseAndSend(context, view) } + if (node.width > 0 && node.height > 0) { + bitmapPool = BitmapPool(node.width, node.height) + } + weakView?.get()?.let { view -> processUpdateWithSnapshot(context, view) } } listener = ViewTreeObserver.OnPreDrawListener { - throttleSend(nodeRef) + throttledUpdate(nodeRef) true } @@ -52,7 +56,7 @@ class DecorViewObserver(val context: Context) : TreeObserver() { // It can be the case that the DecorView the current observer owns has already // drawn. In this case, manually trigger an update. - throttleSend(nodeRef) + throttledUpdate(nodeRef) } override fun unsubscribe() { @@ -61,8 +65,12 @@ class DecorViewObserver(val context: Context) : TreeObserver() { listener.let { nodeRef?.get()?.viewTreeObserver?.removeOnPreDrawListener(it) listener = null - nodeRef = null } + + nodeRef = null + + bitmapPool?.recycle() + bitmapPool = null } } 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 index bd9ed9d99..cd89e2ce8 100644 --- 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 @@ -9,7 +9,10 @@ package com.facebook.flipper.plugins.uidebugger.observers import android.util.Log import com.facebook.flipper.plugins.uidebugger.LogTag +import com.facebook.flipper.plugins.uidebugger.common.BitmapPool import com.facebook.flipper.plugins.uidebugger.core.Context +import com.facebook.flipper.plugins.uidebugger.descriptors.Id +import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId /* @@ -23,11 +26,12 @@ import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId * 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 + * The parent is responsible for detecting when a child observer needs to be cleaned up. */ abstract class TreeObserver { protected val children: MutableMap> = mutableMapOf() + protected var bitmapPool: BitmapPool? = null abstract val type: String @@ -35,44 +39,63 @@ abstract class TreeObserver { abstract fun unsubscribe() - /** - * Optional helper method that traverses the layout hierarchy while managing any encountered child - * observers correctly - */ - fun traverseAndSend(context: Context, root: Any) { - val start = System.currentTimeMillis() - val (visitedNodes, observerRootsNodes) = context.layoutTraversal.traverse(root) + fun processUpdate(context: Context, root: Any) = this.processUpdate(context, root, false) + fun processUpdateWithSnapshot(context: Context, root: Any) = + this.processUpdate(context, root, true) + + /** Traverses the layout hierarchy while managing any encountered child observers. */ + fun processUpdate(context: Context, root: Any, takeSnapshot: Boolean) { + val startTimestamp = System.currentTimeMillis() + val (visitedNodes, observableRoots) = context.layoutTraversal.traverse(root) // Add any new observers - for (observerRoot in observerRootsNodes) { - if (!children.containsKey(observerRoot.nodeId())) { - context.observerFactory.createObserver(observerRoot, context)?.let { childObserver -> + observableRoots.forEach { observable -> + if (!children.containsKey(observable.nodeId())) { + context.observerFactory.createObserver(observable, context)?.let { observer -> Log.d( LogTag, - "Observer ${this.type} discovered new child of type ${childObserver.type} Node ID ${observerRoot.nodeId()}") - childObserver.subscribe(observerRoot) - children[observerRoot.nodeId()] = childObserver + "Observer ${this.type} discovered new child of type ${observer.type} Node ID ${observable.nodeId()}") + observer.subscribe(observable) + children[observable.nodeId()] = observer } + } else { + children[observable.nodeId()]?.subscribe(observable) } } // Remove any old observers - val observerRootIds = observerRootsNodes.map { it.nodeId() } - for (childKey in children.keys) { - if (!observerRootIds.contains(childKey)) { - children[childKey]?.let { childObserver -> + val observableRootsIdentifiers = observableRoots.map { it.nodeId() } + val removables = mutableListOf() + children.keys.forEach { key -> + if (!observableRootsIdentifiers.contains(key)) { + children[key]?.let { observer -> Log.d( LogTag, - "Observer ${this.type} cleaning up child of type ${childObserver.type} Node ID $childKey") + "Observer ${this.type} cleaning up child of type ${observer.type} Node ID $key") - childObserver.cleanUpRecursive() + observer.cleanUpRecursive() } + removables.add(key) } } + removables.forEach { key -> children.remove(key) } Log.d(LogTag, "For Observer ${this.type} Sending ${visitedNodes.size}") - context.treeObserverManager.send( - SubtreeUpdate(type, visitedNodes, start, System.currentTimeMillis())) + + var recyclableBitmap: BitmapPool.RecyclableBitmap? = null + if (takeSnapshot && bitmapPool != null) { + @Suppress("unchecked_cast") + val descriptor = + context.descriptorRegister.descriptorForClassUnsafe(root::class.java) + as NodeDescriptor + recyclableBitmap = bitmapPool?.getBitmap() + descriptor.getSnapshot(root, recyclableBitmap?.bitmap) + } + + val endTimestamp = System.currentTimeMillis() + context.treeObserverManager.enqueueUpdate( + SubtreeUpdate( + type, root.nodeId(), visitedNodes, startTimestamp, endTimestamp, recyclableBitmap)) } fun cleanUpRecursive() { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserverManager.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserverManager.kt index fbb4de18b..a5dae485f 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserverManager.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserverManager.kt @@ -7,12 +7,19 @@ package com.facebook.flipper.plugins.uidebugger.observers +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.util.Base64 +import android.util.Base64OutputStream import android.util.Log import com.facebook.flipper.plugins.uidebugger.LogTag +import com.facebook.flipper.plugins.uidebugger.common.BitmapPool import com.facebook.flipper.plugins.uidebugger.core.Context +import com.facebook.flipper.plugins.uidebugger.descriptors.Id 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 java.io.ByteArrayOutputStream import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel @@ -20,9 +27,11 @@ import kotlinx.serialization.json.Json data class SubtreeUpdate( val observerType: String, + val rootId: Id, val nodes: List, val startTime: Long, - val traversalCompleteTime: Long + val traversalCompleteTime: Long, + val snapshot: BitmapPool.RecyclableBitmap? ) /** Holds the root observer and manages sending updates to desktop */ @@ -34,7 +43,7 @@ class TreeObserverManager(val context: Context) { private val workerScope = CoroutineScope(Dispatchers.IO) private val txId = AtomicInteger() - fun send(update: SubtreeUpdate) { + fun enqueueUpdate(update: SubtreeUpdate) { treeUpdates.trySend(update) } @@ -42,6 +51,7 @@ class TreeObserverManager(val context: Context) { * 1. Sets up the root observer * 2. Starts worker to listen to channel, which serializers and sends data over connection */ + @SuppressLint("NewApi") fun start() { treeUpdates = Channel(Channel.UNLIMITED) @@ -51,16 +61,34 @@ class TreeObserverManager(val context: Context) { workerScope.launch { while (isActive) { try { - val treeUpdate = treeUpdates.receive() - val onWorkerThread = System.currentTimeMillis() - val txId = txId.getAndIncrement().toLong() - val serialized = - Json.encodeToString( - SubtreeUpdateEvent.serializer(), - SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.nodes)) + + var serialized: String? + if (treeUpdate.snapshot == null) { + serialized = + Json.encodeToString( + SubtreeUpdateEvent.serializer(), + SubtreeUpdateEvent( + txId, treeUpdate.observerType, treeUpdate.rootId, treeUpdate.nodes)) + } else { + val stream = ByteArrayOutputStream() + val base64Stream = Base64OutputStream(stream, Base64.DEFAULT) + treeUpdate.snapshot.bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, base64Stream) + val snapshot = stream.toString() + serialized = + Json.encodeToString( + SubtreeUpdateEvent.serializer(), + SubtreeUpdateEvent( + txId, + treeUpdate.observerType, + treeUpdate.rootId, + treeUpdate.nodes, + snapshot)) + + treeUpdate.snapshot.recycle() + } val serializationEnd = System.currentTimeMillis() @@ -80,13 +108,13 @@ class TreeObserverManager(val context: Context) { serializationComplete = serializationEnd, socketComplete = socketEnd, nodesCount = treeUpdate.nodes.size) + context.connectionRef.connection?.send( PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats)) } catch (e: CancellationException) {} catch (e: java.lang.Exception) { Log.e(LogTag, "Unexpected Error in channel ", e) } } - Log.i(LogTag, "Shutting down worker") } }