Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send transaction memory stats in profile payload #2447

Merged
merged 36 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7195171
added IMemoryCollector, with different implementations for Java and A…
stefanosiano Dec 28, 2022
a70ace8
updated changelog
stefanosiano Dec 28, 2022
f14d9e8
get/set MemoryCollector methods marked as @Internal
stefanosiano Dec 29, 2022
9a5e80e
added list of MemoryCollectionData to Android profiles payload as mea…
stefanosiano Dec 30, 2022
ba6113d
updated changelog
stefanosiano Dec 30, 2022
554b4f3
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 3, 2023
dbca5e1
added ICpuCollector to collect cpu usage info while transactions are …
stefanosiano Jan 10, 2023
4a07cc2
updated changelog
stefanosiano Jan 10, 2023
71fccaf
Merge branch 'main' into feat/cpu-usage-collection
stefanosiano Jan 10, 2023
4d55da0
Format code
getsentry-bot Jan 10, 2023
0b6a872
the TransactionPerformanceCollector instance is now put in the option…
stefanosiano Jan 10, 2023
f13bf79
added some comments
stefanosiano Jan 10, 2023
1f7ad91
updated changelog
stefanosiano Jan 10, 2023
0bce6a6
Format code
getsentry-bot Jan 10, 2023
c5d99b4
cpu usage percentage is sent as a double from 0 to 100
stefanosiano Jan 10, 2023
a122617
Merge branch 'main' into feat/cpu-usage-collection
stefanosiano Jan 10, 2023
7289765
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 11, 2023
26ff09c
Merge branch 'fix/single-performance-collector' into feat/android-pro…
stefanosiano Jan 12, 2023
7f96d92
Merge branch 'feat/cpu-usage-collection' into feat/android-profiles-m…
stefanosiano Jan 12, 2023
208e3c9
updated profile measurement keys and unit
stefanosiano Jan 12, 2023
faf9209
Merge branch 'main' into feat/cpu-usage-collection
stefanosiano Jan 12, 2023
9f2e916
TransactionPerformanceCollector now read cpuCollector on start method
stefanosiano Jan 12, 2023
024ce08
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 13, 2023
7ed610c
Merge branch 'feat/cpu-usage-collection' into feat/android-profiles-m…
stefanosiano Jan 13, 2023
abe9a46
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 16, 2023
827b0a7
updated changelog
stefanosiano Jan 16, 2023
b589e09
updated test
stefanosiano Jan 16, 2023
85b30e6
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 19, 2023
e166ef0
updated tests
stefanosiano Jan 19, 2023
18e315e
added final keywords
stefanosiano Jan 23, 2023
aaf35f7
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 23, 2023
b76aa76
cleaned up code
stefanosiano Jan 23, 2023
ad1f037
cleaned up code
stefanosiano Jan 23, 2023
161d834
added line in test for codecov
stefanosiano Jan 23, 2023
f35a636
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 24, 2023
8e01539
Merge branch 'main' into feat/android-profiles-memory-measurement
stefanosiano Jan 25, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Send transaction memory stats in profile payload ([#2447](https://github.com/getsentry/sentry-java/pull/2447))
- Improve ANR implementation: ([#2475](https://github.com/getsentry/sentry-java/pull/2475))
- Add `abnormal_mechanism` to sessions for ANR rate calculation
- Always attach thread dump to ANR events
Expand Down
6 changes: 6 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/ICpuCollector {
public fun <init> (Lio/sentry/ILogger;Lio/sentry/android/core/BuildInfoProvider;)V
public fun collect ()Lio/sentry/CpuCollectionData;
public fun setup ()V
}

public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package io.sentry.android.core;

import android.annotation.SuppressLint;
import android.os.Build;
import android.os.SystemClock;
import android.system.Os;
import android.system.OsConstants;
import io.sentry.CpuCollectionData;
import io.sentry.ICpuCollector;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.util.FileUtils;
import io.sentry.util.Objects;
import java.io.File;
import java.io.IOException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

// 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
@ApiStatus.Internal
public final class AndroidCpuCollector implements ICpuCollector {

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 final @NotNull BuildInfoProvider buildInfoProvider;
private boolean isEnabled = false;

public AndroidCpuCollector(
final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) {
this.logger = Objects.requireNonNull(logger, "Logger is required.");
this.buildInfoProvider =
Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required.");
}

@SuppressLint("NewApi")
@Override
public void setup() {
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) {
isEnabled = false;
return;
}
isEnabled = true;
clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK);
numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF);
nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
lastCpuNanos = readTotalCpuNanos();
}

@SuppressLint("NewApi")
@Override
public @Nullable CpuCollectionData collect() {
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP || !isEnabled) {
return null;
}
long nowNanos = SystemClock.elapsedRealtimeNanos();
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
long realTimeNanosDiff = nowNanos - lastRealtimeNanos;
lastRealtimeNanos = nowNanos;
long cpuNanos = readTotalCpuNanos();
long cpuNanosDiff = cpuNanos - lastCpuNanos;
lastCpuNanos = cpuNanos;
// Later we need to divide the percentage by the number of cores, otherwise we could
// get a percentage value higher than 1. We also want to send the percentage as a
// number from 0 to 100, so we are going to multiply it by 100
double cpuUsagePercentage = cpuNanosDiff / (double) realTimeNanosDiff;

return new CpuCollectionData(
System.currentTimeMillis(), (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.ERROR, "Unable to read /proc/self/stat file. Disabling cpu collection.", e);
}
if (stat != null) {
stat = stat.trim();
String[] stats = stat.split("[\n\t\r ]");
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
// 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);
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ static void initializeIntegrationsAndProcessors(
}
options.setMainThreadChecker(AndroidMainThreadChecker.getInstance());
options.setMemoryCollector(new AndroidMemoryCollector());
options.setCpuCollector(new AndroidCpuCollector(options.getLogger(), buildInfoProvider));
}

private static void installDefaultIntegrations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import io.sentry.IHub;
import io.sentry.ITransaction;
import io.sentry.ITransactionProfiler;
import io.sentry.MemoryCollectionData;
import io.sentry.PerformanceCollectionData;
import io.sentry.ProfilingTraceData;
import io.sentry.ProfilingTransactionData;
import io.sentry.SentryLevel;
Expand Down Expand Up @@ -233,7 +235,7 @@ public void onFrameMetricCollected(
options
.getExecutorService()
.schedule(
() -> timedOutProfilingData = onTransactionFinish(transaction, true),
() -> timedOutProfilingData = onTransactionFinish(transaction, true, null),
PROFILING_TIMEOUT_MILLIS);

transactionStartNanos = SystemClock.elapsedRealtimeNanos();
Expand All @@ -247,11 +249,12 @@ public void onFrameMetricCollected(

@Override
public @Nullable synchronized ProfilingTraceData onTransactionFinish(
final @NotNull ITransaction transaction) {
final @NotNull ITransaction transaction,
final @Nullable PerformanceCollectionData performanceCollectionData) {
try {
return options
.getExecutorService()
.submit(() -> onTransactionFinish(transaction, false))
.submit(() -> onTransactionFinish(transaction, false, performanceCollectionData))
.get();
} catch (ExecutionException e) {
options.getLogger().log(SentryLevel.ERROR, "Error finishing profiling: ", e);
Expand All @@ -263,7 +266,9 @@ public void onFrameMetricCollected(

@SuppressLint("NewApi")
private @Nullable ProfilingTraceData onTransactionFinish(
final @NotNull ITransaction transaction, final boolean isTimeout) {
final @NotNull ITransaction transaction,
final boolean isTimeout,
final @Nullable PerformanceCollectionData memoryCollectionData) {

// onTransactionStart() is only available since Lollipop
// and SystemClock.elapsedRealtimeNanos() since Jelly Bean
Expand Down Expand Up @@ -383,6 +388,7 @@ public void onFrameMetricCollected(
ProfileMeasurement.ID_SCREEN_FRAME_RATES,
new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements));
}
putPerformanceCollectionDataInMeasurements(memoryCollectionData);

// cpu max frequencies are read with a lambda because reading files is involved, so it will be
// done in the background when the trace file is read
Expand All @@ -408,6 +414,43 @@ public void onFrameMetricCollected(
measurementsMap);
}

private void putPerformanceCollectionDataInMeasurements(
final @Nullable PerformanceCollectionData performanceCollectionData) {
if (performanceCollectionData != null) {
List<MemoryCollectionData> memoryCollectionData = performanceCollectionData.getMemoryData();
final @NotNull ArrayDeque<ProfileMeasurementValue> memoryUsageMeasurements =
new ArrayDeque<>();
final @NotNull ArrayDeque<ProfileMeasurementValue> nativeMemoryUsageMeasurements =
new ArrayDeque<>();
for (MemoryCollectionData memoryData : memoryCollectionData) {
if (memoryData.getUsedHeapMemory() > -1) {
memoryUsageMeasurements.add(
new ProfileMeasurementValue(
TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis())
- transactionStartNanos,
memoryData.getUsedHeapMemory()));
}
if (memoryData.getUsedNativeMemory() > -1) {
nativeMemoryUsageMeasurements.add(
new ProfileMeasurementValue(
TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis())
- transactionStartNanos,
memoryData.getUsedNativeMemory()));
}
}
if (!memoryUsageMeasurements.isEmpty()) {
measurementsMap.put(
ProfileMeasurement.ID_MEMORY_FOOTPRINT,
new ProfileMeasurement(ProfileMeasurement.UNIT_BYTES, memoryUsageMeasurements));
}
if (!nativeMemoryUsageMeasurements.isEmpty()) {
measurementsMap.put(
ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT,
new ProfileMeasurement(ProfileMeasurement.UNIT_BYTES, nativeMemoryUsageMeasurements));
}
}
}

/**
* Get MemoryInfo object representing the memory state of the application.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.sentry.android.core

import android.os.Build
import io.sentry.ILogger
import io.sentry.test.getCtor
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class AndroidCpuCollectorTest {

private val className = "io.sentry.android.core.AndroidCpuCollector"
private val ctorTypes = arrayOf(ILogger::class.java, BuildInfoProvider::class.java)
private val fixture = Fixture()

private class Fixture {
private val mockBuildInfoProvider = mock<BuildInfoProvider>()
init {
whenever(mockBuildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP)
}
fun getSut(buildInfoProvider: BuildInfoProvider = mockBuildInfoProvider) =
AndroidCpuCollector(mock(), buildInfoProvider)
}

@Test
fun `when null param is provided, invalid argument is thrown`() {
val ctor = className.getCtor(ctorTypes)

assertFailsWith<IllegalArgumentException> {
ctor.newInstance(arrayOf(null, mock<BuildInfoProvider>()))
}
assertFailsWith<IllegalArgumentException> {
ctor.newInstance(arrayOf(mock<ILogger>(), null))
}
}

@Test
fun `collect works only after setup`() {
val data = fixture.getSut().collect()
assertNull(data)
}

@Test
fun `when collect, both native and heap memory are collected`() {
val collector = fixture.getSut()
collector.setup()
val data = collector.collect()
assertNotNull(data)
assertNotEquals(0.0, data.cpuUsagePercentage)
assertNotEquals(0, data.timestampMillis)
}

@Test
fun `collector works only on api 21+`() {
val mockBuildInfoProvider = mock<BuildInfoProvider>()
whenever(mockBuildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT)
val collector = fixture.getSut(mockBuildInfoProvider)
collector.setup()
val data = collector.collect()
assertNull(data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ class AndroidMemoryCollectorTest {
assertNotEquals(-1, data.usedNativeMemory)
assertEquals(usedNativeMemory, data.usedNativeMemory)
assertEquals(usedMemory, data.usedHeapMemory)
assertNotEquals(0, data.timestamp)
assertNotEquals(0, data.timestampMillis)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -518,4 +518,11 @@ class AndroidOptionsInitializerTest {

assertTrue { fixture.sentryOptions.memoryCollector is AndroidMemoryCollector }
}

@Test
fun `AndroidCpuCollector is set to options`() {
fixture.initSut()

assertTrue { fixture.sentryOptions.cpuCollector is AndroidCpuCollector }
}
}
Loading