Skip to content

Commit f82c73c

Browse files
authored
Merge pull request #2363 from bugsnag/PLAT-15439/remove-heartbeat-lock
Replace the heartbeat lock in AppHang Plugin
2 parents a671aa1 + abbe723 commit f82c73c

2 files changed

Lines changed: 30 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
* Added support for Turbo Module native stacktraces in ``bugsnag-plugin-react-native`
88
[#2367](https://github.com/bugsnag/bugsnag-android/pull/2367)
99

10+
### Bug fixes
11+
12+
* Replaced the heartbeat lock with park/unpark in the [bugsnag-plugin-android-apphang](bugsnag-plugin-android-apphang) so that the main/monitor threads are not interdependant
13+
[#2363](https://github.com/bugsnag/bugsnag-android/pull/2363)
14+
1015
## 6.21.0 (2026-01-05)
1116

1217
### Enhancements

bugsnag-plugin-android-apphang/src/main/java/com/bugsnag/android/internal/LooperMonitorThread.kt

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import android.os.Looper
55
import android.os.SystemClock
66
import java.util.concurrent.TimeUnit
77
import java.util.concurrent.atomic.AtomicBoolean
8-
import java.util.concurrent.locks.ReentrantLock
8+
import java.util.concurrent.locks.LockSupport
99

1010
internal class LooperMonitorThread(
1111
watchedLooper: Looper,
@@ -14,15 +14,13 @@ internal class LooperMonitorThread(
1414
) : Thread("Bugsnag AppHang Monitor: ${watchedLooper.thread.name}") {
1515
private val handler: Handler = Handler(watchedLooper)
1616

17+
@Volatile
1718
private var lastHeartbeatTimestamp = 0L
1819

1920
private val isRunning = AtomicBoolean(false)
2021

2122
private var isAppHangDetected = false
2223

23-
private val heartbeatLock = ReentrantLock(false)
24-
private val heartbeatCondition = heartbeatLock.newCondition()
25-
2624
private val heartbeat: Runnable = Heartbeat()
2725

2826
private fun calculateTimeToAppHang(now: Long): Long =
@@ -36,22 +34,17 @@ internal class LooperMonitorThread(
3634

3735
fun stopMonitoring() {
3836
if (isRunning.compareAndSet(true, false)) {
39-
interrupt()
37+
handler.removeCallbacks(heartbeat)
38+
LockSupport.unpark(this)
4039
}
4140
}
4241

4342
internal fun resetHeartbeatTimer() {
44-
heartbeatLock.lock()
45-
try {
46-
heartbeatCondition.signalAll()
47-
} finally {
48-
heartbeatLock.unlock()
49-
}
43+
LockSupport.unpark(this)
5044
}
5145

5246
private fun reportAppHang(timeSinceLastHeartbeat: Long) {
5347
if (isAppHangDetected) {
54-
// avoid reporting duplicate AppHangs
5548
return
5649
}
5750

@@ -63,37 +56,34 @@ internal class LooperMonitorThread(
6356
handler.post(heartbeat)
6457

6558
while (isRunning.get()) {
66-
heartbeatLock.lock()
67-
try {
68-
val waitThreshold =
69-
if (lastHeartbeatTimestamp <= 0L) appHangThresholdMillis
70-
else calculateTimeToAppHang(SystemClock.elapsedRealtime())
71-
heartbeatCondition.await(waitThreshold, TimeUnit.MILLISECONDS)
72-
73-
val timeSinceLastHeartbeat = SystemClock.elapsedRealtime() - lastHeartbeatTimestamp
74-
75-
if (timeSinceLastHeartbeat >= appHangThresholdMillis) {
76-
reportAppHang(timeSinceLastHeartbeat)
77-
}
78-
} catch (_: InterruptedException) {
79-
// continue loop and check isRunning
80-
} finally {
81-
heartbeatLock.unlock()
59+
val waitThreshold =
60+
if (lastHeartbeatTimestamp <= 0L) appHangThresholdMillis
61+
else calculateTimeToAppHang(SystemClock.uptimeMillis())
62+
63+
val waitThresholdNanos = TimeUnit.MILLISECONDS.toNanos(waitThreshold)
64+
LockSupport.parkNanos(waitThresholdNanos)
65+
66+
if (!isRunning.get()) break
67+
68+
val timeSinceLastHeartbeat = SystemClock.uptimeMillis() - lastHeartbeatTimestamp
69+
70+
if (timeSinceLastHeartbeat >= appHangThresholdMillis) {
71+
reportAppHang(timeSinceLastHeartbeat)
72+
}
73+
74+
if (!handler.post(heartbeat)) {
75+
// handler.post returning false means the Looper has likely quit
76+
isRunning.set(false)
8277
}
8378
}
8479
}
8580

8681
private inner class Heartbeat : Runnable {
8782
override fun run() {
88-
lastHeartbeatTimestamp = SystemClock.elapsedRealtime()
89-
// mark the hang as "recovered" and start the detection again
83+
lastHeartbeatTimestamp = SystemClock.uptimeMillis()
9084
isAppHangDetected = false
91-
resetHeartbeatTimer()
9285

93-
// only post the Heartbeat messages if the monitor is still running
94-
if (isRunning.get()) {
95-
handler.post(this)
96-
}
86+
resetHeartbeatTimer()
9787
}
9888

9989
override fun toString(): String {

0 commit comments

Comments
 (0)