perf(android): Read cpu time via Process.getElapsedCpuTime - #5926
Merged
Conversation
AndroidCpuCollector.collect() runs 10 times per second for the whole duration of every transaction, and each sample read and parsed /proc/self/stat: the file reader buffers, the intermediate strings and a regex split of all 52 fields allocated roughly 25 kB per sample, on top of five syscalls, to obtain four numbers. Process.getElapsedCpuTime() is a @CriticalNative wrapper around clock_gettime(CLOCK_PROCESS_CPUTIME_ID) and returns the same quantity with no allocation, at millisecond rather than clock-tick resolution. It excludes the cpu time of reaped child processes, which an app process does not have. This also drops the Pattern.compile() that ran on the SentryAndroid.init path whether or not performance collection started. Measured on a Pixel 3 (Android 12): collect() drops from 33623 to 16 bytes allocated per call, the remainder being the Double boxing of the result. The reported percentages are unchanged for 1, 4 and 8 of 8 busy cores. Also seed lastRealtimeNanos in setup(), so the first sample is measured against the previous sample instead of against time since boot. Under full load that sample reported 0.0% before and 92.9% after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The percentage calculation still follows the linked article; only the source of the process cpu time changed. Drop the measured allocation sizes from the comment - they belong in the commit history, not next to code that no longer allocates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runningcode
marked this pull request as ready for review
August 10, 2026 15:43
runningcode
requested review from
0xadam-brown,
adinauer,
markushi and
romtsn
as code owners
August 10, 2026 15:43
📲 Install BuildsAndroid
|
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 343.31 ms | 417.23 ms | 73.91 ms |
| d15471f | 286.65 ms | 314.68 ms | 28.03 ms |
| d217708 | 409.83 ms | 474.72 ms | 64.89 ms |
| d500866 | 326.13 ms | 378.70 ms | 52.58 ms |
| fcec2f2 | 314.96 ms | 373.66 ms | 58.70 ms |
| d501a7e | 314.55 ms | 343.34 ms | 28.79 ms |
| 7414e9b | 322.49 ms | 378.88 ms | 56.39 ms |
| fcec2f2 | 357.47 ms | 447.32 ms | 89.85 ms |
| a416a65 | 316.52 ms | 359.67 ms | 43.15 ms |
| 983e0f0 | 350.64 ms | 386.44 ms | 35.79 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 0 B | 0 B | 0 B |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| d217708 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| d500866 | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
| d501a7e | 0 B | 0 B | 0 B |
| 7414e9b | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
| a416a65 | 1.58 MiB | 2.12 MiB | 555.26 KiB |
| 983e0f0 | 0 B | 0 B | 0 B |
romtsn
reviewed
Aug 11, 2026
romtsn
approved these changes
Aug 11, 2026
The linked article describes reading and parsing /proc/self/stat, which this collector no longer does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5925
Linear: #5925
📜 Description
TL;DR We can get the elapsed CPU time by calling
android.os.Process.getElapsedCpuTime()instead of parsing it from a file. This is more performant and uses less memory and fixes one small bug. I hope it will reduce per device variation/issues. Let's see!Ok on to the clanker generated details:
AndroidCpuCollectorobtained the process cpu time by reading/proc/self/statand parsingutime/stime/cutime/cstimeout of it. It now callsandroid.os.Process.getElapsedCpuTime(), which AOSP implements as a@CriticalNativewrapper aroundclock_gettime(CLOCK_PROCESS_CPUTIME_ID).That removes, per sample:
FileReader/BufferedReaderbuffers (an 8 kBStreamDecoderbyte buffer plus a 16 kBchar[]), theStringBuilder, the lineStringand thetoString()copy/proc/self/stat— aMatcher, anArrayListand ~52Strings, of which 4 were usedreadTextstats the file three times viaexists/isFile/canReadbefore the open)It also removes the
Pattern.compile()from the constructor, which ran on theSentryAndroid.initpath whether or not performance collection ever started, along with the_SC_CLK_TCKsysconf and the IO/parse error handling thatisEnabled = falseexisted for.The new source excludes the cpu time of reaped child processes, which an app process does not have, and has millisecond rather than clock-tick (10 ms) resolution.
Second, unrelated fix in the same file:
setup()seededlastCpuNanosbut notlastRealtimeNanos, so the first sample of every transaction divided the cpu delta by the time since device boot and reported ~0%.No public API change (
apiDumpproduces no diff).💡 Motivation and Context
collect()runs every 100 ms for the whole duration of every transaction. On a Pixel 3 each call allocated 33623 bytes, i.e. ~336 kB/s of garbage while a transaction is in flight, to read four numbers out of a ~300 byte file.collect()The remaining 16 bytes are the
Doubleboxing insetCpuUsagePercentage, which this PR does not touch.💚 How did you test it?
Existing unit tests pass. They can't cover the value itself — with
isReturnDefaultValues = truethe android.jar stubs return 0 for both clocks, sowhen collect cpu is collectedreally assertsNaN != 0.0, before and after this change.So verification was done on a Pixel 3 (blueline, Android 12 / API 31, 8 cores) by dexing the class files out of the release build output and driving them with
app_process.1.
getElapsedCpuTime()agrees with/proc/self/stat:The unit is milliseconds, it counts all threads, and it tracks the
/procvalue to within one clock tick (10 ms) — the residual is the tick quantization on the/procside.2. The collector reports the same percentages as before, sampled at the 100 ms interval
DefaultCompositePerformanceCollectoruses:(The low first value in each row is the load threads ramping up inside that 100 ms window.) The pre-change collector run through the same harness produces the same numbers.
3. The first sample after
setup(), taken under full load:4. Allocation and timing, via
Debug.getRuntimeStat("art.gc.bytes-allocated")over 20kcollect()calls, two runs each — the table above. Noteapp_processruns imageless (JIT-cold), so the absolute µs figures are inflated; the byte counts are exact.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
AndroidMemoryCollectorruns on the same 100 ms timer and is worth the same look.