From 7b41e020e89aba92d2d5036a5de8ab90505aec56 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 10 Jan 2025 16:43:12 +0200 Subject: [PATCH] Basic watchdog that triggers thread dumps on puck jank --- .../LocationComponentAnimationActivity.kt | 39 +++++++- .../mapbox/maps/testapp/examples/Watchdog.kt | 93 +++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/mapbox/maps/testapp/examples/Watchdog.kt diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/LocationComponentAnimationActivity.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/LocationComponentAnimationActivity.kt index 1af6b1db0c..ff5fe96d3b 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/LocationComponentAnimationActivity.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/LocationComponentAnimationActivity.kt @@ -1,8 +1,11 @@ package com.mapbox.maps.testapp.examples +import android.animation.Animator +import android.animation.ValueAnimator import android.os.Bundle import android.os.Handler import android.os.Looper +import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.mapbox.geojson.Point @@ -33,6 +36,28 @@ class LocationComponentAnimationActivity : AppCompatActivity() { private inner class FakeLocationProvider : LocationProvider { private var locationConsumer: LocationConsumer? = null + private val listeners = + object : ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + Watchdog.reschedule() + } + + override fun onAnimationStart(animation: Animator) { + Watchdog.reschedule() + } + + override fun onAnimationEnd(animation: Animator) { + Watchdog.stop() + animation.removeListener(this) + (animation as ValueAnimator).removeUpdateListener(this) + } + + override fun onAnimationCancel(animation: Animator) { + } + + override fun onAnimationRepeat(animation: Animator) { + } + } private fun emitFakeLocations() { // after several first emits we update puck animator options @@ -73,7 +98,10 @@ class LocationComponentAnimationActivity : AppCompatActivity() { POINT_LNG + delta, POINT_LAT + delta ) - ) + ) { + addUpdateListener(this@FakeLocationProvider.listeners) + addListener(this@FakeLocationProvider.listeners) + } } } locationConsumer?.onBearingUpdated(BEARING + delta * 10000.0 * 5) @@ -88,11 +116,20 @@ class LocationComponentAnimationActivity : AppCompatActivity() { override fun registerLocationConsumer(locationConsumer: LocationConsumer) { this.locationConsumer = locationConsumer emitFakeLocations() + Watchdog.enabled = true + // Fake a busy main thread after 15s + handler.postDelayed({ + Log.d("TAG", "emitFakeLocations: Blocking main thread") + // Simulate main thread busy for few milliseconds + Thread.sleep(150) + Log.d("TAG", "emitFakeLocations: Finished blocking main thread") + }, 15_000L) } override fun unRegisterLocationConsumer(locationConsumer: LocationConsumer) { this.locationConsumer = null handler.removeCallbacksAndMessages(null) + Watchdog.enabled = false } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/Watchdog.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/Watchdog.kt new file mode 100644 index 0000000000..841fcb3d49 --- /dev/null +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/Watchdog.kt @@ -0,0 +1,93 @@ +package com.mapbox.maps.testapp.examples + +import android.os.Handler +import android.os.HandlerThread +import android.os.Message +import android.os.Process +import android.util.Log +import com.mapbox.maps.testapp.examples.Watchdog.TIME_TO_FIRST_TRIGGER +import com.mapbox.maps.testapp.examples.Watchdog.TIME_TO_SUBSEQUENT_TRIGGER +import com.mapbox.maps.testapp.examples.Watchdog.reschedule +import com.mapbox.maps.testapp.examples.Watchdog.stop + +/** + * A simple watchdog that will trigger a [Process.SIGNAL_QUIT] signal if [reschedule] is not called + * within [TIME_TO_FIRST_TRIGGER] milliseconds and continues to do so every + * [TIME_TO_SUBSEQUENT_TRIGGER] until [reschedule] or [stop] is called. + */ +object Watchdog { + var enabled = false + set(value) { + if (field == value) return + field = value + if (value) { + // Start the watchdog thread when enabled to avoid unnecessary overhead + watchdogHandlerThread = HandlerThread(TAG).apply { start() } + watchdogHandler = Handler(watchdogHandlerThread!!.looper) + } else { + // Stop the watchdog thread and free properties when disabled to avoid unnecessary overhead + stop() + watchdogHandlerThread?.quit() + watchdogHandler = null + watchdogHandlerThread = null + } + } + + private var watchdogHandlerThread: HandlerThread? = null + private var watchdogHandler: Handler? = null + private var currentCounter = 0 + + private val quitSignalTask: () -> Unit = { + Log.w(TAG, "(${currentCounter++}) Task not rescheduled on time. Triggering SIGNAL_QUIT.") + // Send a quit signal to the current process to write a thread dump to `/data/anr/`. + Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT) + scheduleQuitSignalTask() + } + + private fun scheduleQuitSignalTask() { + if (currentCounter >= MAX_CONSECUTIVE_TRIGGERS) { + Log.w( + TAG, + "Max consecutive triggers ($currentCounter) reached. Not scheduling another trigger." + ) + return + } + if (enabled) { + watchdogHandler?.let { + it.sendMessageDelayed(Message.obtain(it, quitSignalTask), TIME_TO_SUBSEQUENT_TRIGGER) + } + } + } + + fun reschedule() { + if (enabled) { + stop() + watchdogHandler?.let { + it.sendMessageDelayed(Message.obtain(it, quitSignalTask), TIME_TO_FIRST_TRIGGER) + } + } + } + + fun stop() { + // Cancel all pending tasks + watchdogHandler?.removeCallbacksAndMessages(null) + currentCounter = 0 + } + + private const val TAG = "Watchdog" + + /** + * The amount of time that need to pass before the watchdog triggers the first time. + * That is, if [reschedule] is not called within this time, the watchdog task will trigger. + * Unit is milliseconds. + */ + private const val TIME_TO_FIRST_TRIGGER: Long = 50L + + /** + * The amount of time that the task will wait before running again. + * Unit is milliseconds. + */ + private const val TIME_TO_SUBSEQUENT_TRIGGER: Long = 100L + + private const val MAX_CONSECUTIVE_TRIGGERS = 5 +} \ No newline at end of file