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
This commit is contained in:
Lorenzo Blasa
2022-09-27 13:00:04 -07:00
committed by Facebook GitHub Bot
parent 7017d74821
commit 433061d377
6 changed files with 106 additions and 46 deletions

View File

@@ -19,7 +19,7 @@ class LithoViewTreeObserver(val context: Context) : TreeObserver<LithoView>() {
override val type = "Litho" override val type = "Litho"
var nodeRef: LithoView? = null private var nodeRef: LithoView? = null
override fun subscribe(node: Any) { override fun subscribe(node: Any) {
@@ -27,7 +27,7 @@ class LithoViewTreeObserver(val context: Context) : TreeObserver<LithoView>() {
nodeRef = node as LithoView nodeRef = node as LithoView
val listener: (view: LithoView) -> Unit = { traverseAndSend(context, node) } val listener: (view: LithoView) -> Unit = { processUpdate(context, node) }
node.setOnDirtyMountListener(listener) node.setOnDirtyMountListener(listener)
listener(node) listener(node)

View File

@@ -17,7 +17,13 @@ data class InitEvent(val rootId: Id) {
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class SubtreeUpdateEvent(val txId: Long, val observerType: String, val nodes: List<Node>) { data class SubtreeUpdateEvent(
val txId: Long,
val observerType: String,
val rootId: Id,
val nodes: List<Node>,
val snapshot: String? = null
) {
companion object { companion object {
const val name = "subtreeUpdate" const val name = "subtreeUpdate"
} }

View File

@@ -35,18 +35,13 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRe
override fun onRootViewsChanged(rootViews: List<View>) { override fun onRootViewsChanged(rootViews: List<View>) {
Log.i(LogTag, "Root views updated, num ${rootViews.size}") Log.i(LogTag, "Root views updated, num ${rootViews.size}")
traverseAndSend(context, applicationRef) processUpdate(context, applicationRef)
} }
} }
context.applicationRef.rootsResolver.attachListener(rootViewListener) context.applicationRef.rootsResolver.attachListener(rootViewListener)
// On subscribe, trigger a traversal on whatever roots we have // On subscribe, trigger a traversal on whatever roots we have
rootViewListener.onRootViewsChanged(applicationRef.rootViews) 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.rootViews.size} root views")
Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities") Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities")
} }

View File

@@ -11,6 +11,7 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.LogTag 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.core.Context
import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@@ -37,14 +38,17 @@ class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
Log.i(LogTag, "Subscribing to decor view changes") Log.i(LogTag, "Subscribing to decor view changes")
val throttleSend = val throttledUpdate =
throttleLatest<WeakReference<View>?>(throttleTimeMs, waitScope, mainScope) { weakView -> throttleLatest<WeakReference<View>?>(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 = listener =
ViewTreeObserver.OnPreDrawListener { ViewTreeObserver.OnPreDrawListener {
throttleSend(nodeRef) throttledUpdate(nodeRef)
true true
} }
@@ -52,7 +56,7 @@ class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
// It can be the case that the DecorView the current observer owns has already // It can be the case that the DecorView the current observer owns has already
// drawn. In this case, manually trigger an update. // drawn. In this case, manually trigger an update.
throttleSend(nodeRef) throttledUpdate(nodeRef)
} }
override fun unsubscribe() { override fun unsubscribe() {
@@ -61,8 +65,12 @@ class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
listener.let { listener.let {
nodeRef?.get()?.viewTreeObserver?.removeOnPreDrawListener(it) nodeRef?.get()?.viewTreeObserver?.removeOnPreDrawListener(it)
listener = null listener = null
nodeRef = null
} }
nodeRef = null
bitmapPool?.recycle()
bitmapPool = null
} }
} }

View File

@@ -9,7 +9,10 @@ package com.facebook.flipper.plugins.uidebugger.observers
import android.util.Log import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag 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.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 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 * 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 * 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<T> { abstract class TreeObserver<T> {
protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf() protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf()
protected var bitmapPool: BitmapPool? = null
abstract val type: String abstract val type: String
@@ -35,44 +39,63 @@ abstract class TreeObserver<T> {
abstract fun unsubscribe() abstract fun unsubscribe()
/** fun processUpdate(context: Context, root: Any) = this.processUpdate(context, root, false)
* Optional helper method that traverses the layout hierarchy while managing any encountered child fun processUpdateWithSnapshot(context: Context, root: Any) =
* observers correctly this.processUpdate(context, root, true)
*/
fun traverseAndSend(context: Context, root: Any) { /** Traverses the layout hierarchy while managing any encountered child observers. */
val start = System.currentTimeMillis() fun processUpdate(context: Context, root: Any, takeSnapshot: Boolean) {
val (visitedNodes, observerRootsNodes) = context.layoutTraversal.traverse(root) val startTimestamp = System.currentTimeMillis()
val (visitedNodes, observableRoots) = context.layoutTraversal.traverse(root)
// Add any new observers // Add any new observers
for (observerRoot in observerRootsNodes) { observableRoots.forEach { observable ->
if (!children.containsKey(observerRoot.nodeId())) { if (!children.containsKey(observable.nodeId())) {
context.observerFactory.createObserver(observerRoot, context)?.let { childObserver -> context.observerFactory.createObserver(observable, context)?.let { observer ->
Log.d( Log.d(
LogTag, LogTag,
"Observer ${this.type} discovered new child of type ${childObserver.type} Node ID ${observerRoot.nodeId()}") "Observer ${this.type} discovered new child of type ${observer.type} Node ID ${observable.nodeId()}")
childObserver.subscribe(observerRoot) observer.subscribe(observable)
children[observerRoot.nodeId()] = childObserver children[observable.nodeId()] = observer
} }
} else {
children[observable.nodeId()]?.subscribe(observable)
} }
} }
// Remove any old observers // Remove any old observers
val observerRootIds = observerRootsNodes.map { it.nodeId() } val observableRootsIdentifiers = observableRoots.map { it.nodeId() }
for (childKey in children.keys) { val removables = mutableListOf<Id>()
if (!observerRootIds.contains(childKey)) { children.keys.forEach { key ->
children[childKey]?.let { childObserver -> if (!observableRootsIdentifiers.contains(key)) {
children[key]?.let { observer ->
Log.d( Log.d(
LogTag, 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}") 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<Any>
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() { fun cleanUpRecursive() {

View File

@@ -7,12 +7,19 @@
package com.facebook.flipper.plugins.uidebugger.observers 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 android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag 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.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.Node
import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent
import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent import com.facebook.flipper.plugins.uidebugger.model.SubtreeUpdateEvent
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@@ -20,9 +27,11 @@ import kotlinx.serialization.json.Json
data class SubtreeUpdate( data class SubtreeUpdate(
val observerType: String, val observerType: String,
val rootId: Id,
val nodes: List<Node>, val nodes: List<Node>,
val startTime: Long, val startTime: Long,
val traversalCompleteTime: Long val traversalCompleteTime: Long,
val snapshot: BitmapPool.RecyclableBitmap?
) )
/** Holds the root observer and manages sending updates to desktop */ /** 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 workerScope = CoroutineScope(Dispatchers.IO)
private val txId = AtomicInteger() private val txId = AtomicInteger()
fun send(update: SubtreeUpdate) { fun enqueueUpdate(update: SubtreeUpdate) {
treeUpdates.trySend(update) treeUpdates.trySend(update)
} }
@@ -42,6 +51,7 @@ class TreeObserverManager(val context: Context) {
* 1. Sets up the root observer * 1. Sets up the root observer
* 2. Starts worker to listen to channel, which serializers and sends data over connection * 2. Starts worker to listen to channel, which serializers and sends data over connection
*/ */
@SuppressLint("NewApi")
fun start() { fun start() {
treeUpdates = Channel(Channel.UNLIMITED) treeUpdates = Channel(Channel.UNLIMITED)
@@ -51,16 +61,34 @@ class TreeObserverManager(val context: Context) {
workerScope.launch { workerScope.launch {
while (isActive) { while (isActive) {
try { try {
val treeUpdate = treeUpdates.receive() val treeUpdate = treeUpdates.receive()
val onWorkerThread = System.currentTimeMillis() val onWorkerThread = System.currentTimeMillis()
val txId = txId.getAndIncrement().toLong() val txId = txId.getAndIncrement().toLong()
val serialized =
Json.encodeToString( var serialized: String?
SubtreeUpdateEvent.serializer(), if (treeUpdate.snapshot == null) {
SubtreeUpdateEvent(txId, treeUpdate.observerType, treeUpdate.nodes)) 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() val serializationEnd = System.currentTimeMillis()
@@ -80,13 +108,13 @@ class TreeObserverManager(val context: Context) {
serializationComplete = serializationEnd, serializationComplete = serializationEnd,
socketComplete = socketEnd, socketComplete = socketEnd,
nodesCount = treeUpdate.nodes.size) nodesCount = treeUpdate.nodes.size)
context.connectionRef.connection?.send( context.connectionRef.connection?.send(
PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats)) PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats))
} catch (e: CancellationException) {} catch (e: java.lang.Exception) { } catch (e: CancellationException) {} catch (e: java.lang.Exception) {
Log.e(LogTag, "Unexpected Error in channel ", e) Log.e(LogTag, "Unexpected Error in channel ", e)
} }
} }
Log.i(LogTag, "Shutting down worker") Log.i(LogTag, "Shutting down worker")
} }
} }