@@ -6,14 +6,27 @@ import android.os.SystemClock
66import java.util.concurrent.TimeUnit
77import java.util.concurrent.atomic.AtomicBoolean
88import java.util.concurrent.locks.LockSupport
9+ import kotlin.compareTo
10+ import kotlin.text.compareTo
11+ import kotlin.text.get
12+ import kotlin.text.set
913
1014internal class LooperMonitorThread (
1115 watchedLooper : Looper ,
1216 private val appHangThresholdMillis : Long ,
13- private val onAppHangDetected : (timeSinceLastHeartbeat: Long ) -> Unit
17+ private val samplingThresholdMillis : Long ,
18+ private val samplingRateMillis : Long ,
19+ private val onAppHangDetected : (timeSinceLastHeartbeat: Long , ThreadSampler ? ) -> Unit
1420) : Thread(" Bugsnag AppHang Monitor: ${watchedLooper.thread.name} " ) {
1521 private val handler: Handler = Handler (watchedLooper)
1622
23+ private val threadSampler: ThreadSampler ? =
24+ if (samplingThresholdMillis > 0 ) ThreadSampler (watchedLooper.thread)
25+ else null
26+
27+ @Volatile
28+ private var lastStackSampleTimestamp = 0L
29+
1730 @Volatile
1831 private var lastHeartbeatTimestamp = 0L
1932
@@ -23,9 +36,6 @@ internal class LooperMonitorThread(
2336
2437 private val heartbeat: Runnable = Heartbeat ()
2538
26- private fun calculateTimeToAppHang (now : Long ): Long =
27- (lastHeartbeatTimestamp + appHangThresholdMillis) - now
28-
2939 fun startMonitoring () {
3040 if (isRunning.compareAndSet(false , true )) {
3141 start()
@@ -49,40 +59,87 @@ internal class LooperMonitorThread(
4959 }
5060
5161 isAppHangDetected = true
52- onAppHangDetected(timeSinceLastHeartbeat)
62+ onAppHangDetected(timeSinceLastHeartbeat, threadSampler )
5363 }
5464
5565 override fun run () {
5666 handler.post(heartbeat)
5767
5868 while (isRunning.get()) {
59- val waitThreshold =
60- if (lastHeartbeatTimestamp <= 0L ) appHangThresholdMillis
61- else calculateTimeToAppHang(SystemClock .uptimeMillis())
69+ val now = SystemClock .uptimeMillis()
70+ val timeSinceHeartbeat = now - lastHeartbeatTimestamp
6271
63- val waitThresholdNanos = TimeUnit .MILLISECONDS .toNanos(waitThreshold)
64- LockSupport .parkNanos(waitThresholdNanos)
72+ // Wait until next sample time or hang detection time, whichever comes first
73+ val waitMillis = calculateNextWaitTime(now, timeSinceHeartbeat)
74+ LockSupport .parkNanos(TimeUnit .MILLISECONDS .toNanos(waitMillis))
6575
6676 if (! isRunning.get()) break
6777
68- val timeSinceLastHeartbeat = SystemClock .uptimeMillis() - lastHeartbeatTimestamp
78+ val currentTime = SystemClock .uptimeMillis()
79+ val currentTimeSinceHeartbeat = currentTime - lastHeartbeatTimestamp
80+
81+ if (shouldTakeSample(currentTime, currentTimeSinceHeartbeat)) {
82+ threadSampler?.captureSample()
83+ lastStackSampleTimestamp = currentTime
84+ }
6985
70- if (timeSinceLastHeartbeat >= appHangThresholdMillis) {
71- reportAppHang(timeSinceLastHeartbeat )
86+ if (currentTimeSinceHeartbeat >= appHangThresholdMillis) {
87+ reportAppHang(currentTimeSinceHeartbeat )
7288 }
7389
7490 if (! handler.post(heartbeat)) {
75- // handler.post returning false means the Looper has likely quit
7691 isRunning.set(false )
7792 }
7893 }
7994 }
8095
96+ private fun calculateNextWaitTime (now : Long , timeSinceHeartbeat : Long ): Long {
97+ if (lastHeartbeatTimestamp <= 0L ) return appHangThresholdMillis
98+ if (timeSinceHeartbeat >= appHangThresholdMillis) return Long .MAX_VALUE
99+
100+ val timeToHang = appHangThresholdMillis - timeSinceHeartbeat
101+ if (threadSampler == null ) return timeToHang
102+
103+ return calculateTimeToNextStackSample(now, timeToHang, timeSinceHeartbeat)
104+ }
105+
106+ private fun calculateTimeToNextStackSample (
107+ now : Long ,
108+ timeToHang : Long ,
109+ timeSinceHeartbeat : Long
110+ ): Long {
111+ return if (lastStackSampleTimestamp > 0L ) {
112+ // Already sampling - wait for next sample
113+ val timeToNextSample = samplingRateMillis - (now - lastStackSampleTimestamp)
114+ minOf(timeToNextSample, timeToHang)
115+ } else {
116+ val timeToSamplingStart = samplingThresholdMillis - timeSinceHeartbeat
117+ minOf(timeToSamplingStart, timeToHang)
118+ }
119+ }
120+
121+ private fun shouldTakeSample (currentTime : Long , timeSinceHeartbeat : Long ): Boolean {
122+ if (threadSampler == null ) return false
123+ if (timeSinceHeartbeat < samplingThresholdMillis) return false
124+
125+ val timeSinceLastSample = if (lastStackSampleTimestamp <= 0L ) {
126+ Long .MAX_VALUE
127+ } else {
128+ currentTime - lastStackSampleTimestamp
129+ }
130+
131+ return timeSinceLastSample >= samplingRateMillis
132+ }
133+
81134 private inner class Heartbeat : Runnable {
82135 override fun run () {
83136 lastHeartbeatTimestamp = SystemClock .uptimeMillis()
84137 isAppHangDetected = false
85138
139+ // Reset sampler when thread recovers
140+ threadSampler?.resetSampling()
141+ lastStackSampleTimestamp = 0L
142+
86143 resetHeartbeatTimer()
87144 }
88145
0 commit comments