Repository navigation
ref(android): Measure ANR thresholds on the monotonic clock (JAVA-579) #6041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
201e3b3
6c6540e
ad84a7d
9a3cef0
15bd047
88946b3
94fa716
4a11cb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |
|
|
||
| import android.os.Handler; | ||
| import android.os.Looper; | ||
| import android.os.SystemClock; | ||
| import io.sentry.IDebugManager; | ||
| import io.sentry.ILogger; | ||
| import io.sentry.IScopes; | ||
|
|
@@ -15,12 +14,15 @@ | |
| import io.sentry.SentryOptions; | ||
| import io.sentry.android.core.AppState; | ||
| import io.sentry.android.core.SentryAndroidOptions; | ||
| import io.sentry.time.MonotonicTicker; | ||
| import io.sentry.time.Stopwatch; | ||
| import io.sentry.util.AutoClosableReentrantLock; | ||
| import io.sentry.util.Objects; | ||
| import io.sentry.util.SentryRandom; | ||
| import java.io.Closeable; | ||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
|
|
@@ -44,12 +46,23 @@ public class AnrProfilingIntegration | |
| static final int MAX_NUM_STACKS = (int) (10_000 / POLLING_INTERVAL_MS); | ||
|
|
||
| private final AtomicBoolean enabled = new AtomicBoolean(true); | ||
| private final Runnable updater = () -> lastMainThreadExecutionTime = SystemClock.uptimeMillis(); | ||
|
|
||
| private volatile @Nullable MonotonicTicker ticker; | ||
|
|
||
| @SuppressWarnings("UnnecessaryLambda") | ||
| private final @NotNull Runnable updater = | ||
| () -> { | ||
| final @Nullable MonotonicTicker currentTicker = ticker; | ||
| if (currentTicker != null) { | ||
| lastMainThreadExecutionNanos = currentTicker.tickNanos(); | ||
| } | ||
| }; | ||
|
|
||
| private final @NotNull AutoClosableReentrantLock lifecycleLock = new AutoClosableReentrantLock(); | ||
| private final @NotNull AutoClosableReentrantLock profileManagerLock = | ||
| new AutoClosableReentrantLock(); | ||
|
|
||
| private volatile long lastMainThreadExecutionTime = SystemClock.uptimeMillis(); | ||
| private volatile long lastMainThreadExecutionNanos; | ||
| final AtomicInteger numCollectedStacks = new AtomicInteger(); | ||
| private volatile MainThreadState mainThreadState = MainThreadState.IDLE; | ||
| private volatile @Nullable AnrProfileManager profileManager; | ||
|
|
@@ -62,13 +75,26 @@ public class AnrProfilingIntegration | |
| private volatile @Nullable Handler mainHandler; | ||
| private volatile @Nullable Thread mainThread; | ||
|
|
||
| /** | ||
| * Installs the ticker to measure main-thread stalls with. Also resets the last-execution reading, | ||
| * so a stall is never measured against a tick from a different ticker. | ||
| * | ||
| * <p>Package-private so a test can install a fake ticker after {@link #register}, which {@code | ||
| * SentryAndroidOptions} cannot supply. | ||
| */ | ||
| void installTicker(final @NotNull MonotonicTicker ticker) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably the only weird design about this PR. Open to other ways of doing this!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see two other options, none of them is great:
(In the long run replacing the generic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that having a custom interface is the way to go, but I don't want to blow up the scope of this PR. |
||
| this.ticker = ticker; | ||
| this.lastMainThreadExecutionNanos = ticker.tickNanos(); | ||
| } | ||
|
|
||
| @Override | ||
| public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { | ||
| this.options = | ||
| Objects.requireNonNull( | ||
| (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, | ||
| "SentryAndroidOptions is required"); | ||
| this.logger = options.getLogger(); | ||
| installTicker(options.getMonotonicTicker()); | ||
| this.debugManager = this.options.getDebugManager(); | ||
|
|
||
| if (this.options.isAnrProfilingEnabled()) { | ||
|
|
@@ -223,8 +249,12 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept | |
| return; | ||
| } | ||
|
|
||
| final long now = SystemClock.uptimeMillis(); | ||
| final long diff = now - lastMainThreadExecutionTime; | ||
| final @Nullable MonotonicTicker currentTicker = ticker; | ||
| if (currentTicker == null) { | ||
| return; | ||
| } | ||
| final long diff = | ||
| TimeUnit.NANOSECONDS.toMillis(currentTicker.tickNanos() - lastMainThreadExecutionNanos); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sleep inflates ANR profiling stallsMedium Severity Measuring the stall with Reviewed by Cursor Bugbot for commit 4a11cb6. Configure here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fine for now, we only ever report the profile if there's an ANR available via ApplicationExitInfo APIs. |
||
|
|
||
| if (diff < THRESHOLD_SUSPICION_MS) { | ||
| mainThreadState = MainThreadState.IDLE; | ||
|
|
@@ -253,14 +283,15 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept | |
| && (mainThreadState == MainThreadState.SUSPICIOUS | ||
| || mainThreadState == MainThreadState.ANR_DETECTED)) { | ||
| if (numCollectedStacks.get() < MAX_NUM_STACKS) { | ||
| final long start = SystemClock.uptimeMillis(); | ||
| final @NotNull Stopwatch stopwatch = Stopwatch.started(currentTicker); | ||
| final @NotNull AnrStackTrace trace = | ||
| new AnrStackTrace(System.currentTimeMillis(), mainThread.getStackTrace()); | ||
| final long duration = SystemClock.uptimeMillis() - start; | ||
| if (logger.isEnabled(SentryLevel.DEBUG)) { | ||
| logger.log( | ||
| SentryLevel.DEBUG, | ||
| "AnrWatchdog: capturing main thread stacktrace took " + duration + "ms"); | ||
| "AnrWatchdog: capturing main thread stacktrace took " | ||
| + stopwatch.elapsed(TimeUnit.MILLISECONDS) | ||
| + "ms"); | ||
| } | ||
| addStackTrace(trace); | ||
| } else { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns EITHER a monotonic clock or a wall clock depending on the platform. This is a bad abstraction.
On android it gives you a monotonic clock which is what we want for this class, but the fact that it has
Datein the name but gives you a monotonic clock is quite confusing.