Skip to content

ref(android): Measure ANR thresholds on the uptime clock (JAVA-579) - #6041

Draft
runningcode wants to merge 4 commits into
no/java-576-checkin-stopwatchfrom
no/java-579-anr-uptime-clock
Draft

ref(android): Measure ANR thresholds on the uptime clock (JAVA-579)#6041
runningcode wants to merge 4 commits into
no/java-576-checkin-stopwatchfrom
no/java-579-anr-uptime-clock

Conversation

@runningcode

@runningcode runningcode commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR Stack (Clock semantics hardening)


📜 Description

Moves both ANR detectors off SystemClock.uptimeMillis() readings behind an untyped
ICurrentDateProvider and onto the UptimeClock interface from #6028.

  • ANRWatchDog takes an UptimeClock instead of an ICurrentDateProvider lambda, and tracks the
    main thread's responsiveness window as a Deadline rather than a lastKnownActiveUiTimestampMs
    long plus subtraction.
  • AnrProfilingIntegration reads its suspicion and ANR thresholds from an injected UptimeClock
    instead of calling SystemClock inline, and times stack capture with a Stopwatch.
  • TestTicker's tick is now @Volatile, because these tests advance the clock from the test thread
    while the watchdog thread reads it.

JavaUptimeClock is System.nanoTime(), which is CLOCK_MONOTONIC on Android — the same clock
behind SystemClock.uptimeMillis(). The thresholds are therefore unchanged; only the precision of
the reading and the name on the type change. Nothing serialized is touched.

💡 Motivation and Context

ANR detection is the one place in the SDK where picking the wrong clock invents user-visible events.
uptimeMillis() excludes deep sleep, which is exactly "time the CPU was available to the main
thread". On an elapsed-real-time clock, a 30 s suspend between posting the ticker and checking it
would look like a 30 s frozen UI and report an ANR that never happened. The old
ICurrentDateProvider field type could not tell those two clocks apart; UptimeClock cannot be
handed the wrong one without failing to compile.

This is also what gives UptimeClock a permanent consumer rather than only the behaviour-freeze one
in #6032.

  • resolves: JAVA-579

💚 How did you test it?

./gradlew :sentry-android-core:testReleaseUnitTest :sentry-android-core:apiCheck — green.

New regression test a device suspend does not trip the ANR threshold: the watchdog polls for
hundreds of milliseconds of wall time while the uptime clock stands still, and no ANR is reported.
That is precisely the event an elapsed-real-time clock would fabricate. Its counterpart, the
existing when ANR is detected test, advances the same clock and does report one.

The AnrProfilingIntegration state-machine tests now drive the injected clock instead of
Robolectric's shadow SystemClock, asserting the same IDLE → SUSPICIOUS → ANR_DETECTED transitions
at the same offsets.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

🔮 Next steps

PR 6 of the stack deprecates the legacy ICurrentDateProvider family and the date-reading helpers
now that the internal consumers have moved off them.

⚠️ Merge this PR using a merge commit (not squash), so the rest of the stack keeps a clean history.

@linear-code

linear-code Bot commented Sep 2, 2026

Copy link
Copy Markdown

JAVA-579

@sentry

sentry Bot commented Sep 2, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.54.0 (1) release

⚙️ sentry-android Build Distribution Settings

The ANR tests advance the clock from the test thread while the watchdog
thread reads it, which without volatile is a data race that can leave the
watchdog looking at a stale tick forever.
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.
…AVA-579)

Same reasoning as the watchdog: the suspicion and ANR thresholds must not
count time the device spent suspended. The clock is now injected rather
than read from SystemClock, so the tests drive it directly instead of
going through Robolectric's shadow clock.
@runningcode
runningcode force-pushed the no/java-576-checkin-stopwatch branch from fef63c7 to 9829cc8 Compare September 2, 2026 13:37
@runningcode
runningcode force-pushed the no/java-579-anr-uptime-clock branch from 77eb8c9 to 2065803 Compare September 2, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant