Skip to content

Commit aac7fb1

Browse files
committed
ref(android): Measure ANR thresholds on the monotonic clock (JAVA-579)
The watchdog took its readings from an ICurrentDateProvider lambda over SystemClock.uptimeMillis(). The type named no clock, so a call site could not tell what it was measuring, and the arithmetic -- now minus the last tick, compared against a threshold -- was spelled out inline. MonotonicClock and Deadline replace both: the clock is a named type, and the watchdog asks the question it actually cares about, which is whether the main thread has missed its window. The clock counts deep sleep, which uptimeMillis() did not, so a suspend between posting the ticker and checking it now looks like a missed window. It cannot fabricate an ANR: the watchdog reports only once ActivityManager confirms the process is NOT_RESPONDING, and on resume the main thread runs the ticker that is already queued.
1 parent 97913a8 commit aac7fb1

2 files changed

Lines changed: 54 additions & 30 deletions

File tree

‎sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
import android.app.ActivityManager;
3131
import android.content.Context;
3232
import android.os.Debug;
33-
import android.os.SystemClock;
3433
import io.sentry.ILogger;
3534
import io.sentry.SentryLevel;
36-
import io.sentry.transport.ICurrentDateProvider;
35+
import io.sentry.android.core.internal.time.AndroidMonotonicClock;
36+
import io.sentry.time.Deadline;
37+
import io.sentry.time.MonotonicClock;
3738
import java.util.List;
39+
import java.util.concurrent.TimeUnit;
3840
import java.util.concurrent.atomic.AtomicBoolean;
3941
import org.jetbrains.annotations.NotNull;
4042
import org.jetbrains.annotations.TestOnly;
@@ -46,15 +48,17 @@ final class ANRWatchDog extends Thread {
4648
private final boolean reportInDebug;
4749
private final ANRListener anrListener;
4850
private final MainLooperHandler uiHandler;
49-
private final ICurrentDateProvider timeProvider;
51+
private final MonotonicClock clock;
5052

5153
/** the interval in which we check if there's an ANR, in ms */
5254
private long pollingIntervalMs;
5355

5456
private final long timeoutIntervalMillis;
5557
private final @NotNull ILogger logger;
5658

57-
private volatile long lastKnownActiveUiTimestampMs = 0;
59+
/** How long the main thread has left to run the ticker before we call it an ANR. */
60+
private volatile @NotNull Deadline uiResponsiveUntil;
61+
5862
private final AtomicBoolean reported = new AtomicBoolean(false);
5963

6064
private final @NotNull Context context;
@@ -68,10 +72,8 @@ final class ANRWatchDog extends Thread {
6872
@NotNull ANRListener listener,
6973
@NotNull ILogger logger,
7074
final @NotNull Context context) {
71-
// avoid method refs on Android due to some issues with older AGP setups
72-
// noinspection Convert2MethodRef
7375
this(
74-
() -> SystemClock.uptimeMillis(),
76+
AndroidMonotonicClock.getInstance(),
7577
timeoutIntervalMillis,
7678
500,
7779
reportInDebug,
@@ -83,7 +85,7 @@ final class ANRWatchDog extends Thread {
8385

8486
@TestOnly
8587
ANRWatchDog(
86-
@NotNull final ICurrentDateProvider timeProvider,
88+
@NotNull final MonotonicClock clock,
8789
long timeoutIntervalMillis,
8890
long pollingIntervalMillis,
8991
boolean reportInDebug,
@@ -94,17 +96,18 @@ final class ANRWatchDog extends Thread {
9496

9597
super("|ANR-WatchDog|");
9698

97-
this.timeProvider = timeProvider;
99+
this.clock = clock;
98100
this.timeoutIntervalMillis = timeoutIntervalMillis;
99101
this.pollingIntervalMs = pollingIntervalMillis;
100102
this.reportInDebug = reportInDebug;
101103
this.anrListener = listener;
102104
this.logger = logger;
103105
this.uiHandler = uiHandler;
104106
this.context = context;
107+
this.uiResponsiveUntil = Deadline.after(clock, timeoutIntervalMillis, TimeUnit.MILLISECONDS);
105108
this.ticker =
106109
() -> {
107-
lastKnownActiveUiTimestampMs = timeProvider.getCurrentTimeMillis();
110+
uiResponsiveUntil = Deadline.after(clock, timeoutIntervalMillis, TimeUnit.MILLISECONDS);
108111
reported.set(false);
109112
};
110113

@@ -140,11 +143,8 @@ public void run() {
140143
return;
141144
}
142145

143-
final long unresponsiveDurationMs =
144-
timeProvider.getCurrentTimeMillis() - lastKnownActiveUiTimestampMs;
145-
146146
// If the main thread has not handled ticker, it is blocked. ANR.
147-
if (unresponsiveDurationMs > timeoutIntervalMillis) {
147+
if (uiResponsiveUntil.hasPassed()) {
148148
if (!reportInDebug && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
149149
logger.log(
150150
SentryLevel.DEBUG,

‎sentry-android-core/src/test/java/io/sentry/android/core/ANRWatchDogTest.kt‎

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import android.app.ActivityManager
44
import android.app.ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING
55
import android.app.ActivityManager.ProcessErrorStateInfo.NO_ERROR
66
import android.content.Context
7-
import io.sentry.transport.ICurrentDateProvider
7+
import io.sentry.time.TestMonotonicClock
88
import java.util.concurrent.CountDownLatch
99
import java.util.concurrent.Executors
1010
import java.util.concurrent.TimeUnit
11+
import java.util.concurrent.TimeUnit.MILLISECONDS
1112
import kotlin.test.Test
1213
import kotlin.test.assertEquals
1314
import kotlin.test.assertNotNull
@@ -20,12 +21,12 @@ import org.mockito.kotlin.mock
2021
import org.mockito.kotlin.whenever
2122

2223
class ANRWatchDogTest {
23-
private var currentTimeMs = 0L
24-
private val timeProvider = ICurrentDateProvider { currentTimeMs }
24+
private lateinit var clock: TestMonotonicClock
2525

2626
@Before
2727
fun `setup`() {
28-
currentTimeMs = 12341234
28+
// an arbitrary, non-zero origin: no caller may assume a tick counts from zero
29+
clock = TestMonotonicClock(MILLISECONDS.toNanos(12341234))
2930
}
3031

3132
@Test
@@ -42,8 +43,7 @@ class ANRWatchDogTest {
4243
whenever(handler.thread).thenReturn(thread)
4344
val interval = 10L
4445

45-
val sut =
46-
ANRWatchDog(timeProvider, interval, 1L, true, { a -> anr = a }, mock(), handler, mock())
46+
val sut = ANRWatchDog(clock, interval, 1L, true, { a -> anr = a }, mock(), handler, mock())
4747
val es = Executors.newSingleThreadExecutor()
4848
try {
4949
es.submit { sut.run() }
@@ -53,7 +53,7 @@ class ANRWatchDogTest {
5353
) // Wait until worker posts the job for the "UI thread"
5454
var waitCount = 0
5555
do {
56-
currentTimeMs += 100L
56+
clock.advance(100, MILLISECONDS)
5757
Thread.sleep(100) // Let worker realize this is ANR
5858
} while (anr == null && waitCount++ < 100)
5959

@@ -79,15 +79,14 @@ class ANRWatchDogTest {
7979
whenever(handler.thread).thenReturn(thread)
8080
val interval = 10L
8181

82-
val sut =
83-
ANRWatchDog(timeProvider, interval, 1L, true, { a -> anr = a }, mock(), handler, mock())
82+
val sut = ANRWatchDog(clock, interval, 1L, true, { a -> anr = a }, mock(), handler, mock())
8483
val es = Executors.newSingleThreadExecutor()
8584
try {
8685
es.submit { sut.run() }
8786

8887
var waitCount = 0
8988
do {
90-
currentTimeMs += 100L
89+
clock.advance(100, MILLISECONDS)
9190
Thread.sleep(100) // Let worker realize his runner always runs
9291
} while (!invoked && waitCount++ < 100)
9392

@@ -121,8 +120,7 @@ class ANRWatchDogTest {
121120
val anrs = listOf(stateInfo)
122121
whenever(am.processesInErrorState).thenReturn(anrs)
123122

124-
val sut =
125-
ANRWatchDog(timeProvider, interval, 1L, true, { a -> anr = a }, mock(), handler, context)
123+
val sut = ANRWatchDog(clock, interval, 1L, true, { a -> anr = a }, mock(), handler, context)
126124
val es = Executors.newSingleThreadExecutor()
127125
try {
128126
es.submit { sut.run() }
@@ -132,7 +130,7 @@ class ANRWatchDogTest {
132130
) // Wait until worker posts the job for the "UI thread"
133131
var waitCount = 0
134132
do {
135-
currentTimeMs += 100L
133+
clock.advance(100, MILLISECONDS)
136134
Thread.sleep(100) // Let worker realize this is ANR
137135
} while (anr == null && waitCount++ < 100)
138136

@@ -167,8 +165,7 @@ class ANRWatchDogTest {
167165
val anrs = listOf(stateInfo)
168166
whenever(am.processesInErrorState).thenReturn(anrs)
169167

170-
val sut =
171-
ANRWatchDog(timeProvider, interval, 1L, true, { a -> anr = a }, mock(), handler, context)
168+
val sut = ANRWatchDog(clock, interval, 1L, true, { a -> anr = a }, mock(), handler, context)
172169
val es = Executors.newSingleThreadExecutor()
173170
try {
174171
es.submit { sut.run() }
@@ -178,7 +175,7 @@ class ANRWatchDogTest {
178175
) // Wait until worker posts the job for the "UI thread"
179176
var waitCount = 0
180177
do {
181-
currentTimeMs += 100L
178+
clock.advance(100, MILLISECONDS)
182179
Thread.sleep(100L) // Let worker realize this is ANR
183180
} while (anr == null && waitCount++ < 100)
184181
assertNull(anr) // callback never ran
@@ -187,4 +184,31 @@ class ANRWatchDogTest {
187184
es.shutdown()
188185
}
189186
}
187+
188+
@Test
189+
fun `a device suspend does not trip the ANR threshold`() {
190+
// The uptime clock stands still while the device is suspended, so a suspend cannot be mistaken
191+
// for a blocked main thread. On an elapsed-real-time clock the suspended interval would count
192+
// against the main thread and fabricate an ANR.
193+
var anr: ApplicationNotResponding? = null
194+
val handler = mock<MainLooperHandler>()
195+
val latch = CountDownLatch(1)
196+
whenever(handler.post(any())).then { latch.countDown() }
197+
whenever(handler.thread).thenReturn(mock<Thread>())
198+
199+
// context is a mock, so ActivityManager is absent and any passed deadline reports an ANR
200+
val sut = ANRWatchDog(clock, 10L, 1L, true, { a -> anr = a }, mock(), handler, mock())
201+
val es = Executors.newSingleThreadExecutor()
202+
try {
203+
es.submit { sut.run() }
204+
205+
assertTrue(latch.await(10L, TimeUnit.SECONDS)) // wait until the watchdog is polling
206+
Thread.sleep(200) // hundreds of polls of wall time, none of it uptime
207+
208+
assertNull(anr)
209+
} finally {
210+
sut.interrupt()
211+
es.shutdown()
212+
}
213+
}
190214
}

0 commit comments

Comments
 (0)