Skip to content

Commit f1aaf89

Browse files
committed
perf(core): Store performance measurements as primitives
While a transaction or profile chunk is running, performance data is collected every 100ms for up to 30 seconds. PerformanceCollectionData stored its measurements as boxed Double/Long, using null to mean "no collector reported this". Heap sizes are far outside the Long cache, so each sample allocated a box per measurement. The fields are now primitives with an explicit presence flag, and consumers ask hasUsedHeapMemory() instead of null-checking. A sentinel value was considered instead, but AndroidCpuCollector can legitimately produce NaN when it observes a zero-length interval, so no in-band value is safe. No behavior change: which measurements are reported, and their values, are unchanged. PerformanceCollectionData is @ApiStatus.Internal, so the signature changes in sentry.api are not part of the public surface.
1 parent e4bb886 commit f1aaf89

10 files changed

Lines changed: 91 additions & 66 deletions

File tree

sentry-android-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ dependencies {
115115
testImplementation(libs.androidx.test.ext.junit)
116116
testImplementation(libs.androidx.test.runner)
117117
testImplementation(libs.awaitility.kotlin)
118+
testImplementation(libs.google.truth)
118119
testImplementation(libs.mockito.kotlin)
119120
testImplementation(libs.mockito.inline)
120121
testImplementation(projects.sentryTestSupport)

sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -322,21 +322,21 @@ private void putPerformanceCollectionDataInMeasurements(
322322
for (final @NotNull PerformanceCollectionData data : performanceCollectionData) {
323323
final long nanoTimestamp = data.getNanoTimestamp();
324324
final long relativeStartNs = nanoTimestamp + timestampDiff;
325-
final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage();
326-
final @Nullable Long usedHeapMemory = data.getUsedHeapMemory();
327-
final @Nullable Long usedNativeMemory = data.getUsedNativeMemory();
328325

329-
if (cpuUsagePercentage != null) {
326+
if (data.hasCpuUsagePercentage()) {
330327
cpuUsageMeasurements.add(
331-
new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp));
328+
new ProfileMeasurementValue(
329+
relativeStartNs, data.getCpuUsagePercentage(), nanoTimestamp));
332330
}
333-
if (usedHeapMemory != null) {
331+
if (data.hasUsedHeapMemory()) {
334332
memoryUsageMeasurements.add(
335-
new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp));
333+
new ProfileMeasurementValue(
334+
relativeStartNs, data.getUsedHeapMemory(), nanoTimestamp));
336335
}
337-
if (usedNativeMemory != null) {
336+
if (data.hasUsedNativeMemory()) {
338337
nativeMemoryUsageMeasurements.add(
339-
new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp));
338+
new ProfileMeasurementValue(
339+
relativeStartNs, data.getUsedNativeMemory(), nanoTimestamp));
340340
}
341341
}
342342
}

sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -609,21 +609,20 @@ private static void addPerformanceDataToMeasurements(
609609
final long sampleElapsedRealtimeNanos = elapsedRealtimeNowNanos - nanosSinceSample;
610610
final long relativeStartNs =
611611
sampleElapsedRealtimeNanos - profileStartElapsedRealtimeNanos;
612-
final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage();
613-
final @Nullable Long usedHeapMemory = data.getUsedHeapMemory();
614-
final @Nullable Long usedNativeMemory = data.getUsedNativeMemory();
615-
616-
if (cpuUsagePercentage != null) {
612+
if (data.hasCpuUsagePercentage()) {
617613
cpuUsageMeasurements.addLast(
618-
new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp));
614+
new ProfileMeasurementValue(
615+
relativeStartNs, data.getCpuUsagePercentage(), nanoTimestamp));
619616
}
620-
if (usedHeapMemory != null) {
617+
if (data.hasUsedHeapMemory()) {
621618
memoryUsageMeasurements.addLast(
622-
new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp));
619+
new ProfileMeasurementValue(
620+
relativeStartNs, data.getUsedHeapMemory(), nanoTimestamp));
623621
}
624-
if (usedNativeMemory != null) {
622+
if (data.hasUsedNativeMemory()) {
625623
nativeMemoryUsageMeasurements.addLast(
626-
new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp));
624+
new ProfileMeasurementValue(
625+
relativeStartNs, data.getUsedNativeMemory(), nanoTimestamp));
627626
}
628627
}
629628
}

sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package io.sentry.android.core
22

3+
import com.google.common.truth.Truth.assertThat
34
import io.sentry.ILogger
45
import io.sentry.PerformanceCollectionData
56
import io.sentry.test.getCtor
67
import kotlin.test.Test
78
import kotlin.test.assertFailsWith
8-
import kotlin.test.assertNotEquals
9-
import kotlin.test.assertNotNull
10-
import kotlin.test.assertNull
119
import org.mockito.kotlin.mock
1210

1311
class AndroidCpuCollectorTest {
@@ -30,7 +28,7 @@ class AndroidCpuCollectorTest {
3028
fun `collect works only after setup`() {
3129
val data = PerformanceCollectionData(10)
3230
fixture.getSut().collect(data)
33-
assertNull(data.cpuUsagePercentage)
31+
assertThat(data.hasCpuUsagePercentage()).isFalse()
3432
}
3533

3634
@Test
@@ -39,8 +37,7 @@ class AndroidCpuCollectorTest {
3937
val collector = fixture.getSut()
4038
collector.setup()
4139
collector.collect(data)
42-
val cpuData = data.cpuUsagePercentage
43-
assertNotNull(cpuData)
44-
assertNotEquals(0.0, cpuData)
40+
assertThat(data.hasCpuUsagePercentage()).isTrue()
41+
assertThat(data.cpuUsagePercentage).isNotEqualTo(0.0)
4542
}
4643
}
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package io.sentry.android.core
22

33
import android.os.Debug
4+
import com.google.common.truth.Truth.assertThat
45
import io.sentry.PerformanceCollectionData
56
import kotlin.test.Test
6-
import kotlin.test.assertEquals
7-
import kotlin.test.assertNotEquals
8-
import kotlin.test.assertNotNull
97

108
class AndroidMemoryCollectorTest {
119
private val fixture = Fixture()
@@ -21,10 +19,9 @@ class AndroidMemoryCollectorTest {
2119
val usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize()
2220
val usedMemory = fixture.runtime.totalMemory() - fixture.runtime.freeMemory()
2321
fixture.collector.collect(data)
24-
assertNotNull(data.usedHeapMemory)
25-
assertNotNull(data.usedNativeMemory)
26-
assertNotEquals(-1, data.usedNativeMemory)
27-
assertEquals(usedNativeMemory, data.usedNativeMemory)
28-
assertEquals(usedMemory, data.usedHeapMemory)
22+
assertThat(data.hasUsedHeapMemory()).isTrue()
23+
assertThat(data.hasUsedNativeMemory()).isTrue()
24+
assertThat(data.usedNativeMemory).isEqualTo(usedNativeMemory)
25+
assertThat(data.usedHeapMemory).isEqualTo(usedMemory)
2926
}
3027
}

sentry-android-core/src/test/java/io/sentry/android/core/ChunkMeasurementCollectorTest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,12 @@ class ChunkMeasurementCollectorTest {
111111
*/
112112
private fun futureFrameEndNanos() = System.nanoTime() + TimeUnit.MINUTES.toNanos(1)
113113

114+
/** A null measurement is left unset, as it would be by a collector that did not report it. */
114115
private fun perfData(nanos: Long, cpu: Double?, heap: Long?, native: Long?) =
115116
PerformanceCollectionData(nanos).apply {
116-
cpuUsagePercentage = cpu
117-
usedHeapMemory = heap
118-
usedNativeMemory = native
117+
cpu?.let { cpuUsagePercentage = it }
118+
heap?.let { usedHeapMemory = it }
119+
native?.let { usedNativeMemory = it }
119120
}
120121

121122
private fun assertChunkCounts(

sentry/api/sentry.api

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,13 +2095,16 @@ public final class io/sentry/OutboxSender : io/sentry/IEnvelopeSender {
20952095

20962096
public final class io/sentry/PerformanceCollectionData {
20972097
public fun <init> (J)V
2098-
public fun getCpuUsagePercentage ()Ljava/lang/Double;
2098+
public fun getCpuUsagePercentage ()D
20992099
public fun getNanoTimestamp ()J
2100-
public fun getUsedHeapMemory ()Ljava/lang/Long;
2101-
public fun getUsedNativeMemory ()Ljava/lang/Long;
2102-
public fun setCpuUsagePercentage (Ljava/lang/Double;)V
2103-
public fun setUsedHeapMemory (Ljava/lang/Long;)V
2104-
public fun setUsedNativeMemory (Ljava/lang/Long;)V
2100+
public fun getUsedHeapMemory ()J
2101+
public fun getUsedNativeMemory ()J
2102+
public fun hasCpuUsagePercentage ()Z
2103+
public fun hasUsedHeapMemory ()Z
2104+
public fun hasUsedNativeMemory ()Z
2105+
public fun setCpuUsagePercentage (D)V
2106+
public fun setUsedHeapMemory (J)V
2107+
public fun setUsedNativeMemory (J)V
21052108
}
21062109

21072110
public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown {

sentry/src/main/java/io/sentry/PerformanceCollectionData.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,71 @@
11
package io.sentry;
22

33
import org.jetbrains.annotations.ApiStatus;
4-
import org.jetbrains.annotations.Nullable;
54

5+
/**
6+
* Holds a single performance measurement sample.
7+
*
8+
* <p>Measurements are stored as primitives with a separate presence flag, rather than as boxed
9+
* nullable types, because an instance is created every 100ms for as long as a transaction or
10+
* profile chunk is running.
11+
*/
612
@ApiStatus.Internal
713
public final class PerformanceCollectionData {
8-
private @Nullable Double cpuUsagePercentage = null;
9-
private @Nullable Long usedHeapMemory = null;
10-
private @Nullable Long usedNativeMemory = null;
14+
private double cpuUsagePercentage;
15+
private boolean hasCpuUsagePercentage;
16+
private long usedHeapMemory;
17+
private boolean hasUsedHeapMemory;
18+
private long usedNativeMemory;
19+
private boolean hasUsedNativeMemory;
1120
private final long nanoTimestamp;
1221

1322
public PerformanceCollectionData(final long nanoTimestamp) {
1423
this.nanoTimestamp = nanoTimestamp;
1524
}
1625

1726
/** Set the cpu usage percentage. */
18-
public void setCpuUsagePercentage(final @Nullable Double cpuUsagePercentage) {
27+
public void setCpuUsagePercentage(final double cpuUsagePercentage) {
1928
this.cpuUsagePercentage = cpuUsagePercentage;
29+
this.hasCpuUsagePercentage = true;
2030
}
2131

22-
public @Nullable Double getCpuUsagePercentage() {
32+
/** Only meaningful when {@link #hasCpuUsagePercentage()} is true. */
33+
public double getCpuUsagePercentage() {
2334
return cpuUsagePercentage;
2435
}
2536

26-
public void setUsedHeapMemory(final @Nullable Long usedHeapMemory) {
37+
public boolean hasCpuUsagePercentage() {
38+
return hasCpuUsagePercentage;
39+
}
40+
41+
public void setUsedHeapMemory(final long usedHeapMemory) {
2742
this.usedHeapMemory = usedHeapMemory;
43+
this.hasUsedHeapMemory = true;
2844
}
2945

30-
public @Nullable Long getUsedHeapMemory() {
46+
/** Only meaningful when {@link #hasUsedHeapMemory()} is true. */
47+
public long getUsedHeapMemory() {
3148
return usedHeapMemory;
3249
}
3350

34-
public void setUsedNativeMemory(final @Nullable Long usedNativeMemory) {
51+
public boolean hasUsedHeapMemory() {
52+
return hasUsedHeapMemory;
53+
}
54+
55+
public void setUsedNativeMemory(final long usedNativeMemory) {
3556
this.usedNativeMemory = usedNativeMemory;
57+
this.hasUsedNativeMemory = true;
3658
}
3759

38-
public @Nullable Long getUsedNativeMemory() {
60+
/** Only meaningful when {@link #hasUsedNativeMemory()} is true. */
61+
public long getUsedNativeMemory() {
3962
return usedNativeMemory;
4063
}
4164

65+
public boolean hasUsedNativeMemory() {
66+
return hasUsedNativeMemory;
67+
}
68+
4269
public long getNanoTimestamp() {
4370
return nanoTimestamp;
4471
}

sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,12 @@ class DefaultCompositePerformanceCollectorTest {
173173
assertNotNull(data1)
174174
assertNotNull(data2)
175175
assertNotNull(data3)
176-
assertFalse(data1.mapNotNull { it.usedHeapMemory }.isEmpty())
177-
assertFalse(data1.mapNotNull { it.cpuUsagePercentage }.isEmpty())
178-
assertFalse(data2.mapNotNull { it.usedHeapMemory }.isEmpty())
179-
assertFalse(data2.mapNotNull { it.cpuUsagePercentage }.isEmpty())
180-
assertFalse(data3.mapNotNull { it.usedHeapMemory }.isEmpty())
181-
assertFalse(data3.mapNotNull { it.cpuUsagePercentage }.isEmpty())
176+
assertTrue(data1.any { it.hasUsedHeapMemory() })
177+
assertTrue(data1.any { it.hasCpuUsagePercentage() })
178+
assertTrue(data2.any { it.hasUsedHeapMemory() })
179+
assertTrue(data2.any { it.hasCpuUsagePercentage() })
180+
assertTrue(data3.any { it.hasUsedHeapMemory() })
181+
assertTrue(data3.any { it.hasCpuUsagePercentage() })
182182
}
183183

184184
@Test
@@ -266,8 +266,8 @@ class DefaultCompositePerformanceCollectorTest {
266266
Thread.sleep(300)
267267
val data1 = collector.stop(fixture.transaction1)
268268
assertNotNull(data1)
269-
val memoryData = data1.map { it.usedHeapMemory }
270-
val cpuData = data1.map { it.cpuUsagePercentage }
269+
val memoryData = data1.filter { it.hasUsedHeapMemory() }
270+
val cpuData = data1.filter { it.hasCpuUsagePercentage() }
271271

272272
// The data returned by the collector is not empty
273273
assertFalse(memoryData.isEmpty())

sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package io.sentry
22

3+
import com.google.common.truth.Truth.assertThat
34
import kotlin.test.Test
4-
import kotlin.test.assertEquals
5-
import kotlin.test.assertNull
65

76
class JavaMemoryCollectorTest {
87
private val fixture = Fixture()
@@ -17,8 +16,9 @@ class JavaMemoryCollectorTest {
1716
val data = PerformanceCollectionData(10)
1817
val usedMemory = fixture.runtime.totalMemory() - fixture.runtime.freeMemory()
1918
fixture.collector.collect(data)
20-
assertNull(data.usedNativeMemory)
21-
assertEquals(usedMemory, data.usedHeapMemory)
22-
assertEquals(10, data.nanoTimestamp)
19+
assertThat(data.hasUsedNativeMemory()).isFalse()
20+
assertThat(data.hasUsedHeapMemory()).isTrue()
21+
assertThat(data.usedHeapMemory).isEqualTo(usedMemory)
22+
assertThat(data.nanoTimestamp).isEqualTo(10)
2323
}
2424
}

0 commit comments

Comments
 (0)