Skip to content

[non-fatal-anr-support] Find the right polling interval and sane defaults #6208

Closed

Description

@markushi

We need to find sane defaults, which are a compromise between the level of detail and cpu overhead.

Main configuration knobs

  • polling interval (frequency of checking the main thread, currently every 66ms (ref)
  • the minimum hang duration, after which we start profiling the main thread, currently 1000ms (ref)
  • the sampling interval, currently 66ms (same as polling interval)
  • the minimum hang duration in order to report the hang as a non-fatal ANR (TBD, to be configurable by the end user)

Plan

  • Add a demo screen to our sample app (TBD, maybe it's better to use a real app, https://github.com/Automattic/pocket-casts-android)
  • Re-init the SDK with minimal settings, one with anr profiling enabled, and one without
  • Run automated tests to measure the overhead

Activity

  1. linear-code commented on Oct 1, 2026

    @linear-code
  2. self-assigned this
    on Oct 1, 2026
  3. markushi commented on Oct 2, 2026

    @markushi
    MemberAuthor

    I've created a demo screen and a benchmark test (see the markushi/feat/anr-profiling-overhead branch), and ran them on a Pixel 8a test device.

    Test setup

    • 10 runs with the integration (66 ms polling interval) and 10 runs without it, in alternating order: with, without, with, without, and so on.
    • Each run is one 10s measurement window, with a ~4s main-thread hang (a backlog of queued messages) that starts at t = 2s.
    • All per-unit values are p95 values: 95 % of the measured events were at or below this value.

    Measured per-unit costs (Pixel 8a, 66 ms, p95)

    • Poll rate: every 66 ms, as configured.
    • Idle poll cycle (background thread): 0.53 ms of CPU (median 0.30 ms). This covers the check, the Handler.post calls and waking up from sleep. The check itself takes 0.034 ms.
    • Idle poll cycle (main thread): the CPU cost is not measurable. The run-to-run variation is larger than that difference. This is great news!
    • Extra main-thread wakeups (due to polling): 12.3 per second (average). The other poll messages run in wakeups that frame redraws cause anyway.
    • Profile collection during a hang (background thread), per captured stack: 2.11 ms of CPU and 7.27 ms of wall time. The stack trace capture takes 0.69 ms (0.59 ms CPU), and the write to disk takes 5.68 ms (1.48 ms CPU). The capture pauses the main thread for up to 0.69 ms. The write blocks only the background thread.

    Example use case: an app runs for one minute, no hiccup

    • Background thread: 909 poll cycles × 0.53 ms ≈ 482 ms of CPU. That is 0.8 % of one CPU core. This is an upper limit, because not every cycle reaches the p95 value. With the average cycle cost (0.31 ms), it is ≈ 282 ms.
    • Main thread: ≈ 741 extra wakeups (12.3 per second × 60), and up to 909 if no frames are drawn. The CPU overhead is not measurable, because each wakeup only writes one timestamp.

    Conclusion

    A 66ms polling interval has no measurable influence on the main thread, although we cause 12.3 extra main-thread wakeups per second. Most of the costs are on the background thread: ≈ 282–482 ms of CPU per minute.

    Further optimizations

    We could sync the polling with frame redraws / input dispatches. This way if the app is completely idle, we wouldn't need to poll at all.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions