Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Comment thread
runningcode marked this conversation as resolved.
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();
}

Expand All @@ -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;
}
}
Loading