Skip to content

Commit a13063e

Browse files
committed
ref(android): Measure ANR thresholds on the uptime clock (JAVA-579)
The watchdog took its readings from an ICurrentDateProvider lambda over SystemClock.uptimeMillis(). The type said nothing about which clock it carried, so nothing stopped a caller from handing it an elapsed-real-time provider — and that substitution is not benign here: elapsed real time counts deep sleep, so a device suspended between posting the ticker and checking it would report the suspended interval as a blocked main thread and fabricate an ANR. UptimeClock names the guarantee the detection depends on, and Deadline replaces the hand-rolled "now minus last tick" arithmetic with the question actually being asked: has the main thread missed its window. System.nanoTime() is CLOCK_MONOTONIC on Android, the same clock behind SystemClock.uptimeMillis(), so the thresholds are unchanged.
1 parent 75e85ea commit a13063e

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.time.Deadline;
36+
import io.sentry.time.JavaUptimeClock;
37+
import io.sentry.time.UptimeClock;
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 UptimeClock 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+
JavaUptimeClock.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 UptimeClock 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.TestTicker
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: TestTicker
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 = TestTicker(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)