diff --git a/CHANGELOG.md b/CHANGELOG.md index 6588c0a78f..6baf0dda01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### 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)) +- Store performance measurements as primitives, removing a boxed allocation per measurement per performance sample ([#5935](https://github.com/getsentry/sentry-java/pull/5935)) ### Dependencies diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 23248d6dae..0e3708a89b 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -115,6 +115,7 @@ dependencies { testImplementation(libs.androidx.test.ext.junit) testImplementation(libs.androidx.test.runner) testImplementation(libs.awaitility.kotlin) + testImplementation(libs.google.truth) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) testImplementation(projects.sentryTestSupport) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index f4357b010f..3f569df537 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -322,21 +322,21 @@ private void putPerformanceCollectionDataInMeasurements( for (final @NotNull PerformanceCollectionData data : performanceCollectionData) { final long nanoTimestamp = data.getNanoTimestamp(); final long relativeStartNs = nanoTimestamp + timestampDiff; - final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage(); - final @Nullable Long usedHeapMemory = data.getUsedHeapMemory(); - final @Nullable Long usedNativeMemory = data.getUsedNativeMemory(); - if (cpuUsagePercentage != null) { + if (data.hasCpuUsagePercentage()) { cpuUsageMeasurements.add( - new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getCpuUsagePercentage(), nanoTimestamp)); } - if (usedHeapMemory != null) { + if (data.hasUsedHeapMemory()) { memoryUsageMeasurements.add( - new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getUsedHeapMemory(), nanoTimestamp)); } - if (usedNativeMemory != null) { + if (data.hasUsedNativeMemory()) { nativeMemoryUsageMeasurements.add( - new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getUsedNativeMemory(), nanoTimestamp)); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java index d4260e93dd..731be77433 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java @@ -609,21 +609,20 @@ private static void addPerformanceDataToMeasurements( final long sampleElapsedRealtimeNanos = elapsedRealtimeNowNanos - nanosSinceSample; final long relativeStartNs = sampleElapsedRealtimeNanos - profileStartElapsedRealtimeNanos; - final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage(); - final @Nullable Long usedHeapMemory = data.getUsedHeapMemory(); - final @Nullable Long usedNativeMemory = data.getUsedNativeMemory(); - - if (cpuUsagePercentage != null) { + if (data.hasCpuUsagePercentage()) { cpuUsageMeasurements.addLast( - new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getCpuUsagePercentage(), nanoTimestamp)); } - if (usedHeapMemory != null) { + if (data.hasUsedHeapMemory()) { memoryUsageMeasurements.addLast( - new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getUsedHeapMemory(), nanoTimestamp)); } - if (usedNativeMemory != null) { + if (data.hasUsedNativeMemory()) { nativeMemoryUsageMeasurements.addLast( - new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp)); + new ProfileMeasurementValue( + relativeStartNs, data.getUsedNativeMemory(), nanoTimestamp)); } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt index 1b855aabc6..422eb1ca80 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt @@ -1,13 +1,11 @@ package io.sentry.android.core +import com.google.common.truth.Truth.assertThat import io.sentry.ILogger import io.sentry.PerformanceCollectionData import io.sentry.test.getCtor import kotlin.test.Test import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull import org.mockito.kotlin.mock class AndroidCpuCollectorTest { @@ -30,7 +28,7 @@ class AndroidCpuCollectorTest { fun `collect works only after setup`() { val data = PerformanceCollectionData(10) fixture.getSut().collect(data) - assertNull(data.cpuUsagePercentage) + assertThat(data.hasCpuUsagePercentage()).isFalse() } @Test @@ -39,8 +37,7 @@ class AndroidCpuCollectorTest { val collector = fixture.getSut() collector.setup() collector.collect(data) - val cpuData = data.cpuUsagePercentage - assertNotNull(cpuData) - assertNotEquals(0.0, cpuData) + assertThat(data.hasCpuUsagePercentage()).isTrue() + assertThat(data.cpuUsagePercentage).isNotEqualTo(0.0) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt index 23214c040c..4a7a621ede 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt @@ -1,11 +1,9 @@ package io.sentry.android.core import android.os.Debug +import com.google.common.truth.Truth.assertThat import io.sentry.PerformanceCollectionData import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull class AndroidMemoryCollectorTest { private val fixture = Fixture() @@ -21,10 +19,9 @@ class AndroidMemoryCollectorTest { val usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize() val usedMemory = fixture.runtime.totalMemory() - fixture.runtime.freeMemory() fixture.collector.collect(data) - assertNotNull(data.usedHeapMemory) - assertNotNull(data.usedNativeMemory) - assertNotEquals(-1, data.usedNativeMemory) - assertEquals(usedNativeMemory, data.usedNativeMemory) - assertEquals(usedMemory, data.usedHeapMemory) + assertThat(data.hasUsedHeapMemory()).isTrue() + assertThat(data.hasUsedNativeMemory()).isTrue() + assertThat(data.usedNativeMemory).isEqualTo(usedNativeMemory) + assertThat(data.usedHeapMemory).isEqualTo(usedMemory) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ChunkMeasurementCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ChunkMeasurementCollectorTest.kt index ffc0090746..0841f97496 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ChunkMeasurementCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ChunkMeasurementCollectorTest.kt @@ -111,11 +111,12 @@ class ChunkMeasurementCollectorTest { */ private fun futureFrameEndNanos() = System.nanoTime() + TimeUnit.MINUTES.toNanos(1) + /** A null measurement is left unset, as it would be by a collector that did not report it. */ private fun perfData(nanos: Long, cpu: Double?, heap: Long?, native: Long?) = PerformanceCollectionData(nanos).apply { - cpuUsagePercentage = cpu - usedHeapMemory = heap - usedNativeMemory = native + cpu?.let { cpuUsagePercentage = it } + heap?.let { usedHeapMemory = it } + native?.let { usedNativeMemory = it } } private fun assertChunkCounts( diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 43de4164a4..1a9242b483 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2095,13 +2095,16 @@ public final class io/sentry/OutboxSender : io/sentry/IEnvelopeSender { public final class io/sentry/PerformanceCollectionData { public fun (J)V - public fun getCpuUsagePercentage ()Ljava/lang/Double; + public fun getCpuUsagePercentage ()D public fun getNanoTimestamp ()J - public fun getUsedHeapMemory ()Ljava/lang/Long; - public fun getUsedNativeMemory ()Ljava/lang/Long; - public fun setCpuUsagePercentage (Ljava/lang/Double;)V - public fun setUsedHeapMemory (Ljava/lang/Long;)V - public fun setUsedNativeMemory (Ljava/lang/Long;)V + public fun getUsedHeapMemory ()J + public fun getUsedNativeMemory ()J + public fun hasCpuUsagePercentage ()Z + public fun hasUsedHeapMemory ()Z + public fun hasUsedNativeMemory ()Z + public fun setCpuUsagePercentage (D)V + public fun setUsedHeapMemory (J)V + public fun setUsedNativeMemory (J)V } public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { diff --git a/sentry/src/main/java/io/sentry/PerformanceCollectionData.java b/sentry/src/main/java/io/sentry/PerformanceCollectionData.java index bc32fcd820..365cacbeb6 100644 --- a/sentry/src/main/java/io/sentry/PerformanceCollectionData.java +++ b/sentry/src/main/java/io/sentry/PerformanceCollectionData.java @@ -1,13 +1,22 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; +/** + * Holds a single performance measurement sample. + * + *

Measurements are stored as primitives with a separate presence flag, rather than as boxed + * nullable types, because an instance is created every 100ms for as long as a transaction or + * profile chunk is running. + */ @ApiStatus.Internal public final class PerformanceCollectionData { - private @Nullable Double cpuUsagePercentage = null; - private @Nullable Long usedHeapMemory = null; - private @Nullable Long usedNativeMemory = null; + private double cpuUsagePercentage; + private boolean hasCpuUsagePercentage; + private long usedHeapMemory; + private boolean hasUsedHeapMemory; + private long usedNativeMemory; + private boolean hasUsedNativeMemory; private final long nanoTimestamp; public PerformanceCollectionData(final long nanoTimestamp) { @@ -15,30 +24,48 @@ public PerformanceCollectionData(final long nanoTimestamp) { } /** Set the cpu usage percentage. */ - public void setCpuUsagePercentage(final @Nullable Double cpuUsagePercentage) { + public void setCpuUsagePercentage(final double cpuUsagePercentage) { this.cpuUsagePercentage = cpuUsagePercentage; + this.hasCpuUsagePercentage = true; } - public @Nullable Double getCpuUsagePercentage() { + /** Only meaningful when {@link #hasCpuUsagePercentage()} is true. */ + public double getCpuUsagePercentage() { return cpuUsagePercentage; } - public void setUsedHeapMemory(final @Nullable Long usedHeapMemory) { + public boolean hasCpuUsagePercentage() { + return hasCpuUsagePercentage; + } + + public void setUsedHeapMemory(final long usedHeapMemory) { this.usedHeapMemory = usedHeapMemory; + this.hasUsedHeapMemory = true; } - public @Nullable Long getUsedHeapMemory() { + /** Only meaningful when {@link #hasUsedHeapMemory()} is true. */ + public long getUsedHeapMemory() { return usedHeapMemory; } - public void setUsedNativeMemory(final @Nullable Long usedNativeMemory) { + public boolean hasUsedHeapMemory() { + return hasUsedHeapMemory; + } + + public void setUsedNativeMemory(final long usedNativeMemory) { this.usedNativeMemory = usedNativeMemory; + this.hasUsedNativeMemory = true; } - public @Nullable Long getUsedNativeMemory() { + /** Only meaningful when {@link #hasUsedNativeMemory()} is true. */ + public long getUsedNativeMemory() { return usedNativeMemory; } + public boolean hasUsedNativeMemory() { + return hasUsedNativeMemory; + } + public long getNanoTimestamp() { return nanoTimestamp; } diff --git a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index bd1e584c2d..d259100853 100644 --- a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -173,12 +173,12 @@ class DefaultCompositePerformanceCollectorTest { assertNotNull(data1) assertNotNull(data2) assertNotNull(data3) - assertFalse(data1.mapNotNull { it.usedHeapMemory }.isEmpty()) - assertFalse(data1.mapNotNull { it.cpuUsagePercentage }.isEmpty()) - assertFalse(data2.mapNotNull { it.usedHeapMemory }.isEmpty()) - assertFalse(data2.mapNotNull { it.cpuUsagePercentage }.isEmpty()) - assertFalse(data3.mapNotNull { it.usedHeapMemory }.isEmpty()) - assertFalse(data3.mapNotNull { it.cpuUsagePercentage }.isEmpty()) + assertTrue(data1.any { it.hasUsedHeapMemory() }) + assertTrue(data1.any { it.hasCpuUsagePercentage() }) + assertTrue(data2.any { it.hasUsedHeapMemory() }) + assertTrue(data2.any { it.hasCpuUsagePercentage() }) + assertTrue(data3.any { it.hasUsedHeapMemory() }) + assertTrue(data3.any { it.hasCpuUsagePercentage() }) } @Test @@ -266,8 +266,8 @@ class DefaultCompositePerformanceCollectorTest { Thread.sleep(300) val data1 = collector.stop(fixture.transaction1) assertNotNull(data1) - val memoryData = data1.map { it.usedHeapMemory } - val cpuData = data1.map { it.cpuUsagePercentage } + val memoryData = data1.filter { it.hasUsedHeapMemory() } + val cpuData = data1.filter { it.hasCpuUsagePercentage() } // The data returned by the collector is not empty assertFalse(memoryData.isEmpty()) diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index 5f4aa7fb03..7f2b593b9a 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -1,8 +1,7 @@ package io.sentry +import com.google.common.truth.Truth.assertThat import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull class JavaMemoryCollectorTest { private val fixture = Fixture() @@ -17,8 +16,9 @@ class JavaMemoryCollectorTest { val data = PerformanceCollectionData(10) val usedMemory = fixture.runtime.totalMemory() - fixture.runtime.freeMemory() fixture.collector.collect(data) - assertNull(data.usedNativeMemory) - assertEquals(usedMemory, data.usedHeapMemory) - assertEquals(10, data.nanoTimestamp) + assertThat(data.hasUsedNativeMemory()).isFalse() + assertThat(data.hasUsedHeapMemory()).isTrue() + assertThat(data.usedHeapMemory).isEqualTo(usedMemory) + assertThat(data.nanoTimestamp).isEqualTo(10) } }