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 04dcae555..3e64d9caa 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 @@ -12,45 +12,47 @@ import android.view.View import android.view.ViewTreeObserver import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.core.Context +import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest import java.lang.ref.WeakReference +import kotlinx.coroutines.* typealias DecorView = View /** Responsible for subscribing to updates to the content view of an activity */ class DecorViewObserver(val context: Context) : TreeObserver() { - val throttleTimeMs = 500 + private val throttleTimeMs = 500L private var nodeRef: WeakReference? = null private var listener: ViewTreeObserver.OnPreDrawListener? = null override val type = "DecorView" + private val waitScope = CoroutineScope(Dispatchers.IO) + private val mainScope = CoroutineScope(Dispatchers.Main) + override fun subscribe(node: Any) { node as View nodeRef = WeakReference(node) Log.i(LogTag, "Subscribing to decor view changes") - // TODO: there's a problem with this. Some future changes may have been - // ignored and not sent. Need to keep track of the last one, always and react - // accordingly. - listener = - object : ViewTreeObserver.OnPreDrawListener { - var lastSend = 0L - override fun onPreDraw(): Boolean { - if (System.currentTimeMillis() - lastSend > throttleTimeMs) { - traverseAndSend(context, node) + val throttleSend = + throttleLatest?>(throttleTimeMs, waitScope, mainScope) { weakView -> + weakView?.get()?.let { view -> traverseAndSend(context, view) } + } - lastSend = System.currentTimeMillis() - } - return true - } + listener = + ViewTreeObserver.OnPreDrawListener { + throttleSend(nodeRef) + true } node.viewTreeObserver.addOnPreDrawListener(listener) - // sometimes we are too late to the party and we miss the first draw - listener?.onPreDraw() + + // It can be the case that the DecorView the current observer owns has already + // drawn. In this case, manually trigger an update. + throttleSend(nodeRef) } override fun unsubscribe() { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/scheduler/CoroutineThrottle.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/scheduler/CoroutineThrottle.kt new file mode 100644 index 000000000..1c97ca96f --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/scheduler/CoroutineThrottle.kt @@ -0,0 +1,46 @@ +/* + * 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.scheduler + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Throttle the execution of an executable for the specified interval. + * + * How does it work? + * + * The function `throttleLatest` returns a proxy for the given executable. This proxy captures the + * latest argument/param that was used on the last invocation. If the throttle job does not exist or + * has already completed, then create a new one. + * + * The job will wait on the waiting scope for the given amount of specified ms. Once it finishes + * waiting, then it will execute the given executable on the main scope with the latest captured + * param. + */ +fun throttleLatest( + intervalMs: Long, + waitScope: CoroutineScope, + mainScope: CoroutineScope, + executable: (T) -> Unit +): (T) -> Unit { + var throttleJob: Job? = null + var latestParam: T + return { param: T -> + latestParam = param + if (throttleJob == null || throttleJob?.isCompleted == true) { + throttleJob = + waitScope.launch { + delay(intervalMs) + mainScope.launch { executable(latestParam) } + } + } + } +}