Skip to content

Commit 7b9a98d

Browse files
Merge pull request #2847 from embrace-io/refactor-anr-code
Simplify ANR detection code
2 parents 8f1550e + a505116 commit 7b9a98d

File tree

7 files changed

+30
-131
lines changed

7 files changed

+30
-131
lines changed

embrace-android-instrumentation-anr/src/main/java/io/embrace/android/embracesdk/internal/instrumentation/anr/detection/LooperCompat.java

Lines changed: 0 additions & 46 deletions
This file was deleted.

embrace-android-instrumentation-anr/src/main/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/AnrModuleImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class AnrModuleImpl(args: InstrumentationArgs) : AnrModule {
4646
looper = looper,
4747
anrMonitorWorker = anrMonitorWorker,
4848
clock = args.clock,
49+
action = blockedThreadDetector::onTargetThreadResponse
4950
)
5051
}
5152

@@ -56,6 +57,7 @@ class AnrModuleImpl(args: InstrumentationArgs) : AnrModule {
5657
targetThread = looper.thread,
5758
blockedDurationThreshold = args.configService.anrBehavior.getMinDuration(),
5859
samplingIntervalMs = args.configService.anrBehavior.getSamplingIntervalMs(),
60+
listener = stacktraceSampler,
5961
)
6062
}
6163

embrace-android-instrumentation-anr/src/main/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/EmbraceAnrService.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ internal class EmbraceAnrService(
4141
if (appStateTracker.getAppState() == AppState.BACKGROUND) {
4242
scheduleDelayedBackgroundCheck()
4343
}
44-
livenessCheckScheduler.listener = stacktraceSampler
4544
}
4645

4746
override fun startAnrCapture() {

embrace-android-instrumentation-anr/src/main/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/detection/LivenessCheckScheduler.kt

Lines changed: 15 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,16 @@ import android.os.Debug
44
import android.os.Handler
55
import android.os.Looper
66
import android.os.Message
7-
import android.os.MessageQueue
87
import io.embrace.android.embracesdk.internal.clock.Clock
98
import io.embrace.android.embracesdk.internal.logging.EmbLogger
109
import io.embrace.android.embracesdk.internal.logging.InternalErrorType
1110
import io.embrace.android.embracesdk.internal.worker.BackgroundWorker
12-
import java.util.concurrent.ExecutorService
1311
import java.util.concurrent.ScheduledFuture
1412
import java.util.concurrent.TimeUnit
1513

1614
/**
1715
* The number of milliseconds which the monitor thread is allowed to timeout before we
18-
* assume that the process has been put into the cached state.
19-
*
20-
* All functions in this class MUST be called from the same thread - this is part of the
21-
* synchronization strategy that ensures ANR data is not corrupted.
16+
* assume the process is in a cached state.
2217
*/
2318
private const val MONITOR_THREAD_TIMEOUT_MS = 60000L
2419

@@ -38,13 +33,11 @@ internal const val HEARTBEAT_REQUEST: Int = 34593
3833

3934
/**
4035
* Responsible for scheduling 'heartbeat' checks on a background thread & posting messages on the
41-
* target thread.
42-
*
43-
* If the target thread does not respond within a given time, an ANR is assumed to have happened.
36+
* target thread. If the target thread does not respond within a given time, an ANR is assumed.
4437
*
4538
* This class is responsible solely for the complicated logic of enqueuing a regular message on the
46-
* target thread & scheduling regular checks on a background thread. The [BlockedThreadDetector]
47-
* class is responsible for the actual business logic that checks whether a thread is blocked or not.
39+
* target thread & scheduling regular checks on a background thread. [BlockedThreadDetector]
40+
* is responsible for the business logic that checks whether a thread is blocked.
4841
*/
4942
internal class LivenessCheckScheduler(
5043
private val anrMonitorWorker: BackgroundWorker,
@@ -56,24 +49,17 @@ internal class LivenessCheckScheduler(
5649
private val logger: EmbLogger,
5750
) {
5851

59-
var listener: BlockedThreadListener?
60-
set(value) {
61-
blockedThreadDetector.listener = value
62-
}
63-
get() = blockedThreadDetector.listener
64-
6552
private var monitorFuture: ScheduledFuture<*>? = null
6653

67-
init {
68-
targetThreadHandler.action = blockedThreadDetector::onTargetThreadResponse
69-
}
70-
7154
/**
7255
* Starts monitoring the target thread for blockages.
7356
*/
7457
fun startMonitoringThread() {
7558
if (!state.started.getAndSet(true)) {
76-
scheduleRegularHeartbeats()
59+
val runnable = Runnable(::checkHeartbeat)
60+
runCatching {
61+
monitorFuture = anrMonitorWorker.scheduleAtFixedRate(runnable, 0, intervalMs, TimeUnit.MILLISECONDS)
62+
}
7763
}
7864
}
7965

@@ -88,13 +74,6 @@ internal class LivenessCheckScheduler(
8874
}
8975
}
9076

91-
private fun scheduleRegularHeartbeats() {
92-
val runnable = Runnable(::checkHeartbeat)
93-
runCatching {
94-
monitorFuture = anrMonitorWorker.scheduleAtFixedRate(runnable, 0, intervalMs, TimeUnit.MILLISECONDS)
95-
}
96-
}
97-
9877
private fun stopHeartbeatTask(): Boolean {
9978
monitorFuture?.let { monitorTask ->
10079
if (monitorTask.cancel(false)) {
@@ -136,56 +115,36 @@ internal class LivenessCheckScheduler(
136115
/**
137116
* A [Handler] that processes messages enqueued on the target [Looper]. If a message is not
138117
* processed by this class in a timely manner then it indicates the target thread is blocked
139-
* with too much work.
140-
*
141-
* When this class processes the message it submits the [action] for execution on the supplied
142-
* [ExecutorService].
143-
*
144-
* Basically speaking: if [handleMessage] takes a long time, the monitor thread assumes there is
145-
* an ANR after a certain time threshold. Once [handleMessage] is invoked, the monitor thread
146-
* knows for sure that the target thread is responsive, so resets the timer for any ANRs.
118+
* with too much work. Once this class has processed the message, the ANR is marked as finished.
147119
*/
148120
internal class TargetThreadHandler(
149121
looper: Looper,
150122
private val anrMonitorWorker: BackgroundWorker,
151-
private val messageQueue: MessageQueue? = LooperCompat.getMessageQueue(looper),
152123
private val clock: Clock,
124+
private val action: (time: Long) -> Unit,
153125
) : Handler(looper) {
154126

155-
lateinit var action: (time: Long) -> Unit
156-
157-
@Volatile
158-
var installed: Boolean = false
159-
160-
fun onIdleThread(): Boolean {
161-
onMainThreadUnblocked()
162-
return true
163-
}
164-
165127
override fun handleMessage(msg: Message) {
166128
runCatching {
167129
if (msg.what == HEARTBEAT_REQUEST) {
168-
// We couldn't obtain the target thread message queue. This should not happen,
169-
// but if it does then we just log an internal error & consider the ANR ended at
170-
// this point.
171-
if (messageQueue == null || !installed) {
172-
onMainThreadUnblocked()
173-
}
130+
onIdleThread()
174131
}
175132
}
176133
}
177134

178-
private fun onMainThreadUnblocked() {
135+
fun onIdleThread() {
179136
val timestamp = clock.now()
180137
anrMonitorWorker.submit {
181-
action.invoke(timestamp)
138+
action(timestamp)
182139
}
183140
}
184141
}
185142

186143
/**
187144
* Responsible for deciding whether a thread is blocked or not. The actual scheduling happens in
188145
* [LivenessCheckScheduler] whereas this class contains the business logic.
146+
*
147+
* All functions in this class MUST be called from the same thread.
189148
*/
190149
class BlockedThreadDetector(
191150
private val clock: Clock,
@@ -199,9 +158,6 @@ class BlockedThreadDetector(
199158
/**
200159
* Called when the target thread process the message. This indicates that the target thread is
201160
* responsive and (usually) means an ANR is about to end.
202-
*
203-
* All functions in this class MUST be called from the same thread - this is part of the
204-
* synchronization strategy that ensures ANR data is not corrupted.
205161
*/
206162
fun onTargetThreadResponse(timestamp: Long) {
207163
state.lastTargetThreadResponseMs = timestamp
@@ -221,9 +177,6 @@ class BlockedThreadDetector(
221177
/**
222178
* Called at regular intervals by the monitor thread. We should check whether the
223179
* target thread has been unresponsive & decide whether this means an ANR is happening.
224-
*
225-
* All functions in this class MUST be called from the same thread - this is part of the
226-
* synchronization strategy that ensures ANR data is not corrupted.
227180
*/
228181
fun updateAnrTracking(timestamp: Long) {
229182
if (isDebuggerEnabled()) {
@@ -260,10 +213,7 @@ class BlockedThreadDetector(
260213

261214
/**
262215
* Checks whether the ANR duration threshold has been exceeded or not.
263-
*
264-
* This defaults to the main thread not having processed a message within 1s.
265216
*/
266-
267217
fun isAnrDurationThresholdExceeded(timestamp: Long): Boolean {
268218
val monitorThreadLag = timestamp - state.lastMonitorThreadResponseMs
269219
val targetThreadLag = timestamp - state.lastTargetThreadResponseMs

embrace-android-instrumentation-anr/src/test/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/EmbraceAnrServiceRule.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,19 @@ internal class EmbraceAnrServiceRule<T : ScheduledExecutorService>(
5656
anrExecutorService = scheduledExecutorSupplier.invoke()
5757
state = ThreadMonitoringState(clock)
5858
worker = BackgroundWorker(anrExecutorService)
59-
targetThreadHandler = TargetThreadHandler(
60-
looper = looper,
61-
anrMonitorWorker = worker,
62-
clock = clock
63-
)
6459
blockedThreadDetector = BlockedThreadDetector(
6560
clock = clock,
6661
state = state,
6762
targetThread = Thread.currentThread(),
6863
blockedDurationThreshold = fakeConfigService.anrBehavior.getMinDuration(),
6964
samplingIntervalMs = fakeConfigService.anrBehavior.getSamplingIntervalMs()
7065
)
66+
targetThreadHandler = TargetThreadHandler(
67+
looper = looper,
68+
anrMonitorWorker = worker,
69+
clock = clock,
70+
action = blockedThreadDetector::onTargetThreadResponse
71+
)
7172
livenessCheckScheduler = LivenessCheckScheduler(
7273
anrMonitorWorker = worker,
7374
clock = clock,

embrace-android-instrumentation-anr/src/test/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/detection/LivenessCheckSchedulerTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ internal class LivenessCheckSchedulerTest {
5656
samplingIntervalMs = configService.anrBehavior.getSamplingIntervalMs()
5757
)
5858
fakeTargetThreadHandler = mockk(relaxUnitFun = true) {
59-
every { action = any() } returns Unit
6059
every { sendMessage(any()) } returns true
6160
}
6261
every { fakeTargetThreadHandler.hasMessages(any()) } returns false

embrace-android-instrumentation-anr/src/test/kotlin/io/embrace/android/embracesdk/internal/instrumentation/anr/detection/TargetThreadHandlerTest.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package io.embrace.android.embracesdk.internal.instrumentation.anr.detection
22

33
import android.os.Message
4-
import android.os.MessageQueue
54
import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService
65
import io.embrace.android.embracesdk.fakes.FakeConfigService
76
import io.embrace.android.embracesdk.internal.config.ConfigService
87
import io.embrace.android.embracesdk.internal.worker.BackgroundWorker
98
import io.mockk.mockk
10-
import io.mockk.verify
119
import org.junit.Assert.assertEquals
1210
import org.junit.Assert.assertNotNull
1311
import org.junit.Before
@@ -31,21 +29,19 @@ internal class TargetThreadHandlerTest {
3129
runnable = Runnable {}
3230
configService = FakeConfigService()
3331
anrMonitorThread = AtomicReference()
34-
executorService = BlockingScheduledExecutorService(blockingMode = true)
32+
executorService = BlockingScheduledExecutorService()
3533
executorService.submit { anrMonitorThread.set(Thread.currentThread()) }
3634
executorService.runCurrentlyBlocked()
37-
handler = createHandler(null)
38-
handler.action = mockk()
35+
handler = createHandler()
3936
}
4037

41-
private fun createHandler(messageQueue: MessageQueue?): TargetThreadHandler {
38+
private fun createHandler(): TargetThreadHandler {
4239
return TargetThreadHandler(
4340
mockk(relaxed = true),
4441
BackgroundWorker(executorService),
45-
messageQueue
46-
) { FAKE_TIME_MS }.apply {
42+
{ FAKE_TIME_MS },
4743
action = {}
48-
}
44+
)
4945
}
5046

5147
@Test
@@ -55,7 +51,7 @@ internal class TargetThreadHandlerTest {
5551

5652
// process a message
5753
handler.handleMessage(mockk(relaxed = true))
58-
verify(exactly = 0) { handler.action.invoke(any()) }
54+
assertEquals(1, executorService.submitCount)
5955
}
6056

6157
@Test
@@ -82,13 +78,11 @@ internal class TargetThreadHandlerTest {
8278
handler.handleMessage(msg)
8379
assertEquals(2, executorService.submitCount)
8480
executorService.runCurrentlyBlocked()
85-
verify { handler.action.invoke(FAKE_TIME_MS) }
8681
}
8782

8883
@Test
8984
fun testCorrectMsgNonNullQueue() {
90-
handler = createHandler(mockk(relaxed = true))
91-
handler.installed = true
85+
handler = createHandler()
9286
assertNotNull(handler)
9387
state.lastTargetThreadResponseMs = 0L
9488
state.anrInProgress = true

0 commit comments

Comments
 (0)