Skip to content

Commit e4bb886

Browse files
runningcodeclaude
andauthored
perf(android): Read cpu time via Process.getElapsedCpuTime (#5926)
* perf(android): Read cpu time via Process.getElapsedCpuTime (JAVA-690) 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> * changelog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ref(android): Restore the cpu collector attribution comment 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> * ref(android): Drop the stale cpu collector attribution comment 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 69ee9eb commit e4bb886

2 files changed

Lines changed: 16 additions & 54 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
- Clear contexts when calling `Scope.clear()` ([#5902](https://github.com/getsentry/sentry-java/pull/5902))
1212
- Preserve custom `Throwable` identities when R8 optimizes Android apps ([#5881](https://github.com/getsentry/sentry-java/pull/5881))
13+
- Report the correct cpu usage for the first performance sample of a transaction, which was measured against the time since device boot ([#5926](https://github.com/getsentry/sentry-java/pull/5926))
14+
15+
### Performance
16+
17+
- Reduce allocations while collecting cpu usage during transactions by reading the process cpu time via `Process.getElapsedCpuTime()` instead of parsing `/proc/self/stat` (33.6kB to 16 bytes per sample on a Pixel 3) ([#5926](https://github.com/getsentry/sentry-java/pull/5926))
1318

1419
### Dependencies
1520

Lines changed: 11 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,42 @@
11
package io.sentry.android.core;
22

3+
import android.os.Process;
34
import android.os.SystemClock;
45
import android.system.Os;
56
import android.system.OsConstants;
67
import io.sentry.ILogger;
78
import io.sentry.IPerformanceSnapshotCollector;
89
import io.sentry.PerformanceCollectionData;
9-
import io.sentry.SentryLevel;
10-
import io.sentry.util.FileUtils;
1110
import io.sentry.util.Objects;
12-
import java.io.File;
13-
import java.io.IOException;
14-
import java.util.regex.Pattern;
1511
import org.jetbrains.annotations.ApiStatus;
1612
import org.jetbrains.annotations.NotNull;
1713

18-
// The approach to get the cpu usage info was taken from
19-
// https://eng.lyft.com/monitoring-cpu-performance-of-lyfts-android-applications-4e36fafffe12
20-
// The content of the /proc/self/stat file is specified in
21-
// https://man7.org/linux/man-pages/man5/proc.5.html
14+
// The process cpu time comes from Process.getElapsedCpuTime(), a @CriticalNative wrapper around
15+
// clock_gettime(CLOCK_PROCESS_CPUTIME_ID), rather than from parsing /proc/self/stat: reading and
16+
// parsing that file allocated on every sample, and collect() runs 10 times per second for the whole
17+
// duration of a transaction. It does not include the cpu time of reaped child processes, which an
18+
// app process doesn't have.
2219
@ApiStatus.Internal
2320
public final class AndroidCpuCollector implements IPerformanceSnapshotCollector {
2421

22+
private static final long NANOSECONDS_PER_MILLISECOND = 1_000_000;
23+
2524
private long lastRealtimeNanos = 0;
2625
private long lastCpuNanos = 0;
2726

28-
/** Number of clock ticks per second. */
29-
private long clockSpeedHz = 1;
30-
3127
private long numCores = 1;
32-
private final long NANOSECOND_PER_SECOND = 1_000_000_000;
33-
34-
/** Number of nanoseconds per clock tick. */
35-
private double nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
3628

37-
/** File containing stats about this process. */
38-
private final @NotNull File selfStat = new File("/proc/self/stat");
39-
40-
private final @NotNull ILogger logger;
4129
private boolean isEnabled = false;
42-
private final @NotNull Pattern newLinePattern = Pattern.compile("[\n\t\r ]");
4330

4431
public AndroidCpuCollector(final @NotNull ILogger logger) {
45-
this.logger = Objects.requireNonNull(logger, "Logger is required.");
32+
Objects.requireNonNull(logger, "Logger is required.");
4633
}
4734

4835
@Override
4936
public void setup() {
5037
isEnabled = true;
51-
clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK);
5238
numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF);
53-
nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
39+
lastRealtimeNanos = SystemClock.elapsedRealtimeNanos();
5440
lastCpuNanos = readTotalCpuNanos();
5541
}
5642

@@ -74,36 +60,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti
7460
(cpuUsagePercentage / (double) numCores) * 100.0);
7561
}
7662

77-
/** Read the /proc/self/stat file and parses the result. */
7863
private long readTotalCpuNanos() {
79-
String stat = null;
80-
try {
81-
stat = FileUtils.readText(selfStat);
82-
} catch (IOException e) {
83-
// If an error occurs when reading the file, we avoid reading it again until the setup method
84-
// is called again
85-
isEnabled = false;
86-
logger.log(
87-
SentryLevel.WARNING, "Unable to read /proc/self/stat file. Disabling cpu collection.", e);
88-
}
89-
if (stat != null) {
90-
stat = stat.trim();
91-
String[] stats = newLinePattern.split(stat);
92-
try {
93-
// Amount of clock ticks this process has been scheduled in user mode
94-
long uTime = Long.parseLong(stats[13]);
95-
// Amount of clock ticks this process has been scheduled in kernel mode
96-
long sTime = Long.parseLong(stats[14]);
97-
// Amount of clock ticks this process' waited-for children has been scheduled in user mode
98-
long cuTime = Long.parseLong(stats[15]);
99-
// Amount of clock ticks this process' waited-for children has been scheduled in kernel mode
100-
long csTime = Long.parseLong(stats[16]);
101-
return (long) ((uTime + sTime + cuTime + csTime) * nanosecondsPerClockTick);
102-
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
103-
logger.log(SentryLevel.ERROR, "Error parsing /proc/self/stat file.", e);
104-
return 0;
105-
}
106-
}
107-
return 0;
64+
return Process.getElapsedCpuTime() * NANOSECONDS_PER_MILLISECOND;
10865
}
10966
}

0 commit comments

Comments
 (0)