Skip to content

Commit 76d5e07

Browse files
runningcodeclaude
andcommitted
ref(time): Supply the ANR ticker through MonotonicTickerProvider (JAVA-579)
Both ANR detectors resolved AndroidMonotonicTicker.getInstance() themselves, so each call site named the concrete Android ticker and neither could be pointed at another one. SentryAndroidOptions already knows which ticker the platform wants, but it is final, so a test cannot override the getter. Add a one-method MonotonicTickerProvider that SentryOptions implements. ANRWatchDog gains a static factory that reads its timeout, debug behavior, logger and ticker off options, replacing the telescoping convenience constructor. AnrProfilingIntegration installs the ticker in register(), which drops the two constructors it had grown and leaves the public no-arg constructor and sentry-android-core.api unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 46fc85a commit 76d5e07

7 files changed

Lines changed: 62 additions & 36 deletions

File tree

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,19 @@
3232
import android.os.Debug;
3333
import io.sentry.ILogger;
3434
import io.sentry.SentryLevel;
35-
import io.sentry.android.core.internal.time.AndroidMonotonicTicker;
3635
import io.sentry.time.Deadline;
3736
import io.sentry.time.MonotonicTicker;
3837
import java.util.List;
3938
import java.util.concurrent.TimeUnit;
4039
import java.util.concurrent.atomic.AtomicBoolean;
4140
import org.jetbrains.annotations.NotNull;
42-
import org.jetbrains.annotations.TestOnly;
4341

4442
/** A watchdog timer thread that detects when the UI thread has frozen. */
4543
@SuppressWarnings("UnusedReturnValue")
4644
final class ANRWatchDog extends Thread {
4745

46+
private static final long DEFAULT_POLLING_INTERVAL_MS = 500;
47+
4848
private final boolean reportInDebug;
4949
private final ANRListener anrListener;
5050
private final MainLooperHandler uiHandler;
@@ -66,24 +66,22 @@ final class ANRWatchDog extends Thread {
6666
@SuppressWarnings("UnnecessaryLambda")
6767
private final Runnable ticker;
6868

69-
ANRWatchDog(
70-
long timeoutIntervalMillis,
71-
boolean reportInDebug,
72-
@NotNull ANRListener listener,
73-
@NotNull ILogger logger,
69+
/** Reads the timeout, the debug behavior, the logger and the ticker off {@code options}. */
70+
static @NotNull ANRWatchDog create(
71+
final @NotNull SentryAndroidOptions options,
72+
final @NotNull ANRListener listener,
7473
final @NotNull Context context) {
75-
this(
76-
AndroidMonotonicTicker.getInstance(),
77-
timeoutIntervalMillis,
78-
500,
79-
reportInDebug,
74+
return new ANRWatchDog(
75+
options.getMonotonicTicker(),
76+
options.getAnrTimeoutIntervalMillis(),
77+
DEFAULT_POLLING_INTERVAL_MS,
78+
options.isAnrReportInDebug(),
8079
listener,
81-
logger,
80+
options.getLogger(),
8281
new MainLooperHandler(),
8382
context);
8483
}
8584

86-
@TestOnly
8785
ANRWatchDog(
8886
@NotNull final MonotonicTicker monotonicTicker,
8987
long timeoutIntervalMillis,

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,7 @@ private void startAnrWatchdog(
9595
options.getAnrTimeoutIntervalMillis());
9696

9797
anrWatchDog =
98-
new ANRWatchDog(
99-
options.getAnrTimeoutIntervalMillis(),
100-
options.isAnrReportInDebug(),
101-
error -> reportANR(scopes, options, error),
102-
options.getLogger(),
103-
context);
98+
ANRWatchDog.create(options, error -> reportANR(scopes, options, error), context);
10499
anrWatchDog.start();
105100

106101
options.getLogger().log(SentryLevel.DEBUG, "AnrIntegration installed.");

‎sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfilingIntegration.java‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
import io.sentry.SentryOptions;
1414
import io.sentry.android.core.AppState;
1515
import io.sentry.android.core.SentryAndroidOptions;
16-
import io.sentry.android.core.internal.time.AndroidMonotonicTicker;
16+
import io.sentry.time.JavaMonotonicTicker;
1717
import io.sentry.time.MonotonicTicker;
18+
import io.sentry.time.MonotonicTickerProvider;
1819
import io.sentry.time.Stopwatch;
1920
import io.sentry.util.AutoClosableReentrantLock;
2021
import io.sentry.util.Objects;
@@ -46,8 +47,11 @@ public class AnrProfilingIntegration
4647
static final int MAX_NUM_STACKS = (int) (10_000 / POLLING_INTERVAL_MS);
4748

4849
private final AtomicBoolean enabled = new AtomicBoolean(true);
49-
private final @NotNull MonotonicTicker ticker;
50-
private final @NotNull Runnable updater;
50+
private volatile @NotNull MonotonicTicker ticker = JavaMonotonicTicker.getInstance();
51+
52+
@SuppressWarnings("UnnecessaryLambda")
53+
private final @NotNull Runnable updater = () -> lastMainThreadExecutionNanos = ticker.tickNanos();
54+
5155
private final @NotNull AutoClosableReentrantLock lifecycleLock = new AutoClosableReentrantLock();
5256
private final @NotNull AutoClosableReentrantLock profileManagerLock =
5357
new AutoClosableReentrantLock();
@@ -64,15 +68,16 @@ public class AnrProfilingIntegration
6468
private volatile @Nullable Handler mainHandler;
6569
private volatile @Nullable Thread mainThread;
6670

67-
public AnrProfilingIntegration() {
68-
this(AndroidMonotonicTicker.getInstance());
69-
}
70-
71-
@TestOnly
72-
AnrProfilingIntegration(final @NotNull MonotonicTicker ticker) {
73-
this.ticker = ticker;
71+
/**
72+
* Installs the ticker to measure main-thread stalls with. Also resets the last-execution reading,
73+
* so a stall is never measured against a tick from a different ticker.
74+
*
75+
* <p>Package-private so a test can install a fake ticker after {@link #register}, which {@code
76+
* SentryAndroidOptions} cannot supply.
77+
*/
78+
void installTickerFrom(final @NotNull MonotonicTickerProvider provider) {
79+
this.ticker = provider.getMonotonicTicker();
7480
this.lastMainThreadExecutionNanos = ticker.tickNanos();
75-
this.updater = () -> lastMainThreadExecutionNanos = ticker.tickNanos();
7681
}
7782

7883
@Override
@@ -82,6 +87,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
8287
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
8388
"SentryAndroidOptions is required");
8489
this.logger = options.getLogger();
90+
installTickerFrom(options);
8591

8692
if (this.options.isAnrProfilingEnabled()) {
8793
if (this.options.getCacheDirPath() == null) {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ class AnrProfilingIntegrationTest {
174174
anrProfilingSampleRate = 1.0
175175
}
176176

177-
val integration = AnrProfilingIntegration(ticker)
177+
val integration = AnrProfilingIntegration()
178178
integration.register(mockScopes, androidOptions)
179+
integration.installTickerFrom { ticker }
179180
// Drive the state machine synchronously to avoid racing the background polling thread.
180181

181182
ticker.advance(900, MILLISECONDS)
@@ -209,8 +210,9 @@ class AnrProfilingIntegrationTest {
209210
anrProfilingSampleRate = 1.0
210211
}
211212

212-
val integration = AnrProfilingIntegration(ticker)
213+
val integration = AnrProfilingIntegration()
213214
integration.register(mockScopes, androidOptions)
215+
integration.installTickerFrom { ticker }
214216
integration.onBackground()
215217

216218
ticker.advance(19_000, MILLISECONDS)
@@ -275,8 +277,9 @@ class AnrProfilingIntegrationTest {
275277
anrProfilingSampleRate = 0.0
276278
}
277279

278-
val integration = AnrProfilingIntegration(ticker)
280+
val integration = AnrProfilingIntegration()
279281
integration.register(mockScopes, androidOptions)
282+
integration.installTickerFrom { ticker }
280283
integration.onForeground()
281284

282285
// Transition to suspicious

‎sentry/api/sentry.api‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3654,7 +3654,7 @@ public final class io/sentry/SentryOpenTelemetryMode : java/lang/Enum {
36543654
public static fun values ()[Lio/sentry/SentryOpenTelemetryMode;
36553655
}
36563656

3657-
public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig {
3657+
public class io/sentry/SentryOptions : io/sentry/time/MonotonicTickerProvider, io/sentry/transport/RateLimiterConfig {
36583658
public static final field DEFAULT_PROPAGATION_TARGETS Ljava/lang/String;
36593659
public static final field MAX_EVENT_SIZE_BYTES J
36603660
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
@@ -7626,6 +7626,10 @@ public abstract interface class io/sentry/time/MonotonicTicker {
76267626
public abstract fun tickNanos ()J
76277627
}
76287628

7629+
public abstract interface class io/sentry/time/MonotonicTickerProvider {
7630+
public abstract fun getMonotonicTicker ()Lio/sentry/time/MonotonicTicker;
7631+
}
7632+
76297633
public final class io/sentry/time/Stopwatch {
76307634
public fun elapsed (Ljava/util/concurrent/TimeUnit;)J
76317635
public fun elapsedNanos ()J

‎sentry/src/main/java/io/sentry/SentryOptions.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.sentry.time.EpochClock;
2525
import io.sentry.time.JavaMonotonicTicker;
2626
import io.sentry.time.MonotonicTicker;
27+
import io.sentry.time.MonotonicTickerProvider;
2728
import io.sentry.time.SystemEpochClock;
2829
import io.sentry.transport.ITransport;
2930
import io.sentry.transport.ITransportGate;
@@ -59,7 +60,7 @@
5960

6061
/** Sentry SDK options */
6162
@Open
62-
public class SentryOptions implements RateLimiterConfig {
63+
public class SentryOptions implements RateLimiterConfig, MonotonicTickerProvider {
6364

6465
@ApiStatus.Internal public static final @NotNull String DEFAULT_PROPAGATION_TARGETS = ".*";
6566

@@ -3088,6 +3089,7 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) {
30883089
* which this module cannot reference. On the JVM there is no suspend state to account for.
30893090
*/
30903091
@ApiStatus.Internal
3092+
@Override
30913093
public @NotNull MonotonicTicker getMonotonicTicker() {
30923094
return JavaMonotonicTicker.getInstance();
30933095
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.sentry.time;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
/**
7+
* Supplies the {@link MonotonicTicker} to measure elapsed time with.
8+
*
9+
* <p>{@code SentryOptions} implements this, and the Android module overrides it with a ticker the
10+
* core module cannot reference. Depending on this interface rather than on the options class keeps
11+
* the concrete ticker out of call sites, and lets a test supply its own without subclassing the
12+
* final {@code SentryAndroidOptions}.
13+
*/
14+
@ApiStatus.Internal
15+
public interface MonotonicTickerProvider {
16+
@NotNull
17+
MonotonicTicker getMonotonicTicker();
18+
}

0 commit comments

Comments
 (0)