Skip to content

Commit

Permalink
Basic watchdog that triggers thread dumps on puck jank
Browse files Browse the repository at this point in the history
  • Loading branch information
jush committed Jan 10, 2025
1 parent f30a797 commit 7b41e02
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}

Expand Down
93 changes: 93 additions & 0 deletions app/src/main/java/com/mapbox/maps/testapp/examples/Watchdog.kt
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 7b41e02

Please sign in to comment.