diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ef91d1cc..4b34bf90ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Clear contexts when calling `Scope.clear()` ([#5902](https://github.com/getsentry/sentry-java/pull/5902)) - Preserve custom `Throwable` identities when R8 optimizes Android apps ([#5881](https://github.com/getsentry/sentry-java/pull/5881)) +- 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)) + +### Performance + +- 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)) ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java index ea7a20deab..cb8e148b31 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java @@ -1,56 +1,42 @@ package io.sentry.android.core; +import android.os.Process; import android.os.SystemClock; import android.system.Os; import android.system.OsConstants; import io.sentry.ILogger; import io.sentry.IPerformanceSnapshotCollector; import io.sentry.PerformanceCollectionData; -import io.sentry.SentryLevel; -import io.sentry.util.FileUtils; import io.sentry.util.Objects; -import java.io.File; -import java.io.IOException; -import java.util.regex.Pattern; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -// The approach to get the cpu usage info was taken from -// https://eng.lyft.com/monitoring-cpu-performance-of-lyfts-android-applications-4e36fafffe12 -// The content of the /proc/self/stat file is specified in -// https://man7.org/linux/man-pages/man5/proc.5.html +// The process cpu time comes from Process.getElapsedCpuTime(), a @CriticalNative wrapper around +// clock_gettime(CLOCK_PROCESS_CPUTIME_ID), rather than from parsing /proc/self/stat: reading and +// parsing that file allocated on every sample, and collect() runs 10 times per second for the whole +// duration of a transaction. It does not include the cpu time of reaped child processes, which an +// app process doesn't have. @ApiStatus.Internal public final class AndroidCpuCollector implements IPerformanceSnapshotCollector { + private static final long NANOSECONDS_PER_MILLISECOND = 1_000_000; + private long lastRealtimeNanos = 0; private long lastCpuNanos = 0; - /** Number of clock ticks per second. */ - private long clockSpeedHz = 1; - private long numCores = 1; - private final long NANOSECOND_PER_SECOND = 1_000_000_000; - - /** Number of nanoseconds per clock tick. */ - private double nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz; - /** File containing stats about this process. */ - private final @NotNull File selfStat = new File("/proc/self/stat"); - - private final @NotNull ILogger logger; private boolean isEnabled = false; - private final @NotNull Pattern newLinePattern = Pattern.compile("[\n\t\r ]"); public AndroidCpuCollector(final @NotNull ILogger logger) { - this.logger = Objects.requireNonNull(logger, "Logger is required."); + Objects.requireNonNull(logger, "Logger is required."); } @Override public void setup() { isEnabled = true; - clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK); numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF); - nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz; + lastRealtimeNanos = SystemClock.elapsedRealtimeNanos(); lastCpuNanos = readTotalCpuNanos(); } @@ -74,36 +60,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti (cpuUsagePercentage / (double) numCores) * 100.0); } - /** Read the /proc/self/stat file and parses the result. */ private long readTotalCpuNanos() { - String stat = null; - try { - stat = FileUtils.readText(selfStat); - } catch (IOException e) { - // If an error occurs when reading the file, we avoid reading it again until the setup method - // is called again - isEnabled = false; - logger.log( - SentryLevel.WARNING, "Unable to read /proc/self/stat file. Disabling cpu collection.", e); - } - if (stat != null) { - stat = stat.trim(); - String[] stats = newLinePattern.split(stat); - try { - // Amount of clock ticks this process has been scheduled in user mode - long uTime = Long.parseLong(stats[13]); - // Amount of clock ticks this process has been scheduled in kernel mode - long sTime = Long.parseLong(stats[14]); - // Amount of clock ticks this process' waited-for children has been scheduled in user mode - long cuTime = Long.parseLong(stats[15]); - // Amount of clock ticks this process' waited-for children has been scheduled in kernel mode - long csTime = Long.parseLong(stats[16]); - return (long) ((uTime + sTime + cuTime + csTime) * nanosecondsPerClockTick); - } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { - logger.log(SentryLevel.ERROR, "Error parsing /proc/self/stat file.", e); - return 0; - } - } - return 0; + return Process.getElapsedCpuTime() * NANOSECONDS_PER_MILLISECOND; } }