From ce9e6afe7a30902f06e0e8e275e35dc7c70e7674 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 17 Sep 2024 15:39:20 +0200 Subject: [PATCH 1/5] added profile_chunk envelope create added IHub.captureProfileChunk and ISentryClient.captureProfileChunk added profilerId and chunkId reset logic to AndroidContinuousProfiler added absolute timestamps to ProfileMeasurementValue added ProfileContext to Contexts --- .../core/AndroidContinuousProfiler.java | 56 +++- .../android/core/AndroidCpuCollector.java | 5 +- .../android/core/AndroidMemoryCollector.java | 4 +- .../sentry/android/core/AndroidProfiler.java | 21 +- .../core/AndroidContinuousProfilerTest.kt | 48 ++- .../android/core/AndroidCpuCollectorTest.kt | 1 + .../core/AndroidMemoryCollectorTest.kt | 1 + .../android/core/AndroidProfilerTest.kt | 9 +- .../core/AndroidTransactionProfilerTest.kt | 6 +- .../core/SessionTrackingIntegrationTest.kt | 5 + sentry/api/sentry.api | 98 +++++- .../java/io/sentry/CpuCollectionData.java | 12 +- .../src/main/java/io/sentry/HubAdapter.java | 6 + .../main/java/io/sentry/HubScopesWrapper.java | 5 + sentry/src/main/java/io/sentry/IScopes.java | 10 + .../main/java/io/sentry/ISentryClient.java | 11 + .../java/io/sentry/JavaMemoryCollector.java | 3 +- .../main/java/io/sentry/JsonSerializer.java | 2 + .../java/io/sentry/MainEventProcessor.java | 31 +- .../java/io/sentry/MemoryCollectionData.java | 17 +- sentry/src/main/java/io/sentry/NoOpHub.java | 5 + .../src/main/java/io/sentry/NoOpScopes.java | 5 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/ProfileChunk.java | 317 ++++++++++++++++++ .../main/java/io/sentry/ProfileContext.java | 122 +++++++ sentry/src/main/java/io/sentry/Scopes.java | 29 ++ .../main/java/io/sentry/ScopesAdapter.java | 5 + .../src/main/java/io/sentry/SentryClient.java | 40 ++- .../java/io/sentry/SentryEnvelopeItem.java | 62 +++- .../main/java/io/sentry/SentryItemType.java | 1 + .../src/main/java/io/sentry/SpanContext.java | 6 + .../ProfileMeasurementValue.java | 43 ++- .../java/io/sentry/protocol/Contexts.java | 15 + .../java/io/sentry/protocol/DebugMeta.java | 38 +++ .../src/test/java/io/sentry/HubAdapterTest.kt | 6 + .../java/io/sentry/JavaMemoryCollectorTest.kt | 1 + .../test/java/io/sentry/JsonSerializerTest.kt | 262 +++++++++++++-- sentry/src/test/java/io/sentry/NoOpHubTest.kt | 4 + .../java/io/sentry/NoOpSentryClientTest.kt | 4 + .../sentry/PerformanceCollectionDataTest.kt | 15 +- .../test/java/io/sentry/ScopesAdapterTest.kt | 6 + sentry/src/test/java/io/sentry/ScopesTest.kt | 46 +++ .../test/java/io/sentry/SentryClientTest.kt | 57 +++- .../java/io/sentry/SentryEnvelopeItemTest.kt | 88 +++++ .../java/io/sentry/protocol/ContextsTest.kt | 6 + .../java/io/sentry/protocol/DebugMetaTest.kt | 72 ++++ 46 files changed, 1518 insertions(+), 94 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/ProfileChunk.java create mode 100644 sentry/src/main/java/io/sentry/ProfileContext.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index f8155b38b9..b9cd9ca9da 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -8,8 +8,13 @@ import io.sentry.ILogger; import io.sentry.IScopes; import io.sentry.ISentryExecutorService; +import io.sentry.ProfileChunk; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.protocol.SentryId; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Future; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -32,6 +37,9 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private boolean isRunning = false; private @Nullable IScopes scopes; private @Nullable Future closeFuture; + private final @NotNull List payloadBuilders = new ArrayList<>(); + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + private @NotNull SentryId chunkId = SentryId.EMPTY_ID; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -105,8 +113,17 @@ public synchronized void start() { if (startData == null) { return; } + isRunning = true; + if (profilerId == SentryId.EMPTY_ID) { + profilerId = new SentryId(); + } + + if (chunkId == SentryId.EMPTY_ID) { + chunkId = new SentryId(); + } + closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } @@ -138,14 +155,29 @@ private synchronized void stop(final boolean restartProfiler) { return; } + // The scopes can be null if the profiler is started before the SDK is initialized (app start + // profiling), meaning there's no scopes to send the chunks. In that case, we store the data + // in a list and send it when the next chunk is finished. + synchronized (payloadBuilders) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + } + isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; - // todo schedule capture profile chunk envelope + if (scopes != null) { + sendChunks(scopes, scopes.getOptions()); + } if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); start(); } else { + // When the profiler is stopped manually, we have to reset its id + profilerId = SentryId.EMPTY_ID; logger.log(SentryLevel.DEBUG, "Profile chunk finished."); } } @@ -157,6 +189,28 @@ public synchronized void close() { stop(); } + private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + try { + options + .getExecutorService() + .submit( + () -> { + final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); + synchronized (payloadBuilders) { + for (ProfileChunk.Builder builder : payloadBuilders) { + payloads.add(builder.build(options)); + } + payloadBuilders.clear(); + } + for (ProfileChunk payload : payloads) { + scopes.captureProfileChunk(payload); + } + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e); + } + } + @Override public boolean isRunning() { return isRunning; 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 8f54305e6f..2c9ef38cbb 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 @@ -10,6 +10,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.PerformanceCollectionData; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.util.FileUtils; import io.sentry.util.Objects; import java.io.File; @@ -85,7 +86,9 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti CpuCollectionData cpuData = new CpuCollectionData( - System.currentTimeMillis(), (cpuUsagePercentage / (double) numCores) * 100.0); + System.currentTimeMillis(), + (cpuUsagePercentage / (double) numCores) * 100.0, + new SentryNanotimeDate()); performanceCollectionData.addCpuData(cpuData); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java index f475c1801b..866a1db1a4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java @@ -4,6 +4,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryNanotimeDate; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -18,7 +19,8 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti long now = System.currentTimeMillis(); long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory, usedNativeMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(now, usedMemory, usedNativeMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } 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 3d2e8a9205..75d507246c 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 @@ -11,7 +11,9 @@ import io.sentry.ISentryExecutorService; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryDate; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; import io.sentry.profilemeasurements.ProfileMeasurementValue; @@ -158,6 +160,7 @@ public void onFrameMetricCollected( // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp // relative to profileStartNanos + final SentryDate timestamp = new SentryNanotimeDate(); final long frameTimestampRelativeNanos = frameEndNanos - System.nanoTime() @@ -171,15 +174,18 @@ public void onFrameMetricCollected( } if (isFrozen) { frozenFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } else if (isSlow) { slowFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } if (refreshRate != lastRefreshRate) { lastRefreshRate = refreshRate; screenFrameRateMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, refreshRate, timestamp)); } } }); @@ -326,19 +332,22 @@ private void putPerformanceCollectionDataInMeasurements( cpuUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff, - cpuData.getCpuUsagePercentage())); + cpuData.getCpuUsagePercentage(), + cpuData.getTimestamp())); } if (memoryData != null && memoryData.getUsedHeapMemory() > -1) { memoryUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedHeapMemory())); + memoryData.getUsedHeapMemory(), + memoryData.getTimestamp())); } if (memoryData != null && memoryData.getUsedNativeMemory() > -1) { nativeMemoryUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedNativeMemory())); + memoryData.getUsedNativeMemory(), + memoryData.getTimestamp())); } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index 3efc1f0cf5..5878354e70 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -4,8 +4,8 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryLevel import io.sentry.SentryTracer @@ -45,7 +45,7 @@ class AndroidContinuousProfilerTest { } val mockLogger = mock() - val hub: IHub = mock() + val scopes: IScopes = mock() val frameMetricsCollector: SentryFrameMetricsCollector = mock() lateinit var transaction1: SentryTracer @@ -61,10 +61,10 @@ class AndroidContinuousProfilerTest { fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { optionConfig(options) - whenever(hub.options).thenReturn(options) - transaction1 = SentryTracer(TransactionContext("", ""), hub) - transaction2 = SentryTracer(TransactionContext("", ""), hub) - transaction3 = SentryTracer(TransactionContext("", ""), hub) + whenever(scopes.options).thenReturn(options) + transaction1 = SentryTracer(TransactionContext("", ""), scopes) + transaction2 = SentryTracer(TransactionContext("", ""), scopes) + transaction3 = SentryTracer(TransactionContext("", ""), scopes) return AndroidContinuousProfiler( buildInfoProvider, frameMetricsCollector, @@ -73,7 +73,7 @@ class AndroidContinuousProfilerTest { options.isProfilingEnabled, options.profilingTracesHz, options.executorService - ) + ).also { it.setScopes(scopes) } } } @@ -316,4 +316,38 @@ class AndroidContinuousProfilerTest { verify(fixture.mockLogger, times(2)).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) assertTrue(profiler.isRunning) } + + @Test + fun `profiler sends chunk on each restart`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + // Now the executor is used to send the chunk + executorService.runAll() + verify(fixture.scopes).captureProfileChunk(any()) + } + + @Test + fun `profiler sends another chunk on stop`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + // We stop the profiler, which should send an additional chunk + profiler.stop() + // Now the executor is used to send the chunk + executorService.runAll() + verify(fixture.scopes, times(2)).captureProfileChunk(any()) + } } 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 45010255e8..5df457c700 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 @@ -56,6 +56,7 @@ class AndroidCpuCollectorTest { assertNotNull(cpuData) assertNotEquals(0.0, cpuData.cpuUsagePercentage) assertNotEquals(0, cpuData.timestampMillis) + assertNotEquals(0, cpuData.timestamp.nanoTimestamp()) } @Test 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 7879c2daf5..3f00775d69 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 @@ -28,5 +28,6 @@ class AndroidMemoryCollectorTest { assertEquals(usedNativeMemory, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index c5bb334bb3..e9d11decb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -9,6 +9,7 @@ import io.sentry.ILogger import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData +import io.sentry.SentryDate import io.sentry.SentryExecutorService import io.sentry.SentryLevel import io.sentry.android.core.internal.util.SentryFrameMetricsCollector @@ -278,12 +279,14 @@ class AndroidProfilerTest { val profiler = fixture.getSut() val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + val t1 = mock() + val t2 = mock() + singleData.addMemoryData(MemoryCollectionData(1, 2, 3, t1)) + singleData.addCpuData(CpuCollectionData(1, 1.4, t1)) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(2, 3, 4, t2)) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 32c91547f1..8208038496 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -459,12 +459,12 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + singleData.addMemoryData(MemoryCollectionData(1, 2, 3, mock())) + singleData.addCpuData(CpuCollectionData(1, 1.4, mock())) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(2, 3, 4, mock())) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index e6d3dfadd7..3707aebc62 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -11,6 +11,7 @@ import io.sentry.Hint import io.sentry.IMetricsAggregator import io.sentry.IScope import io.sentry.ISentryClient +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.Sentry import io.sentry.SentryEnvelope @@ -177,6 +178,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureProfileChunk(profileChunk: ProfileChunk, scope: IScope?): SentryId { + TODO("Not yet implemented") + } + override fun captureCheckIn(checkIn: CheckIn, scope: IScope?, hint: Hint?): SentryId { TODO("Not yet implemented") } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 577b94a6d3..b5815cb08b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -308,8 +308,9 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { } public final class io/sentry/CpuCollectionData { - public fun (JD)V + public fun (JDLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getTimestampMillis ()J } @@ -557,6 +558,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -624,6 +626,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -866,6 +869,7 @@ public abstract interface class io/sentry/IScopes { public fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public abstract fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;)Lio/sentry/protocol/SentryId; @@ -947,6 +951,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V public abstract fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -1265,8 +1270,9 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se } public final class io/sentry/MemoryCollectionData { - public fun (JJ)V - public fun (JJJ)V + public fun (JJJLio/sentry/SentryDate;)V + public fun (JJLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getTimestampMillis ()J public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J @@ -1418,6 +1424,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1579,6 +1586,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1844,6 +1852,78 @@ public final class io/sentry/PerformanceCollectionData { public fun getMemoryData ()Lio/sentry/MemoryCollectionData; } +public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun ()V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Lio/sentry/SentryOptions;)V + public fun equals (Ljava/lang/Object;)Z + public fun getChunkId ()Lio/sentry/protocol/SentryId; + public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; + public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; + public fun getEnvironment ()Ljava/lang/String; + public fun getMeasurements ()Ljava/util/Map; + public fun getPlatform ()Ljava/lang/String; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRelease ()Ljava/lang/String; + public fun getSampledProfile ()Ljava/lang/String; + public fun getTraceFile ()Ljava/io/File; + public fun getUnknown ()Ljava/util/Map; + public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setSampledProfile (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileChunk$Builder { + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;)V + public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; +} + +public final class io/sentry/ProfileChunk$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileChunk; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileChunk$JsonKeys { + public static final field CHUNK_ID Ljava/lang/String; + public static final field CLIENT_SDK Ljava/lang/String; + public static final field DEBUG_META Ljava/lang/String; + public static final field ENVIRONMENT Ljava/lang/String; + public static final field MEASUREMENTS Ljava/lang/String; + public static final field PLATFORM Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; + public static final field RELEASE Ljava/lang/String; + public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field VERSION Ljava/lang/String; + public fun ()V +} + +public class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/ProfileContext;)V + public fun (Lio/sentry/protocol/SentryId;)V + public fun equals (Ljava/lang/Object;)Z + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileContext$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileContext; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileContext$JsonKeys { + public static final field PROFILER_ID Ljava/lang/String; + public fun ()V +} + public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TRUNCATION_REASON_BACKGROUNDED Ljava/lang/String; public static final field TRUNCATION_REASON_NORMAL Ljava/lang/String; @@ -2175,6 +2255,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes, io/sentry/metrics/Metri public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2245,6 +2326,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2535,6 +2617,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient, io/sentry/m public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; @@ -2616,6 +2699,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; public static fun fromMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/SentryEnvelopeItem; + public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; @@ -2746,6 +2830,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field ClientReport Lio/sentry/SentryItemType; public static final field Event Lio/sentry/SentryItemType; public static final field Profile Lio/sentry/SentryItemType; + public static final field ProfileChunk Lio/sentry/SentryItemType; public static final field ReplayEvent Lio/sentry/SentryItemType; public static final field ReplayRecording Lio/sentry/SentryItemType; public static final field ReplayVideo Lio/sentry/SentryItemType; @@ -4429,9 +4514,10 @@ public final class io/sentry/profilemeasurements/ProfileMeasurement$JsonKeys { public final class io/sentry/profilemeasurements/ProfileMeasurementValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V - public fun (Ljava/lang/Long;Ljava/lang/Number;)V + public fun (Ljava/lang/Long;Ljava/lang/Number;Lio/sentry/SentryDate;)V public fun equals (Ljava/lang/Object;)Z public fun getRelativeStartNs ()Ljava/lang/String; + public fun getTimestamp ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; public fun getValue ()D public fun hashCode ()I @@ -4447,6 +4533,7 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$Deseria public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKeys { public static final field START_NS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public static final field VALUE Ljava/lang/String; public fun ()V } @@ -4543,6 +4630,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun getDevice ()Lio/sentry/protocol/Device; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; + public fun getProfile ()Lio/sentry/ProfileContext; public fun getResponse ()Lio/sentry/protocol/Response; public fun getRuntime ()Lio/sentry/protocol/SentryRuntime; public fun getSize ()I @@ -4561,6 +4649,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun setDevice (Lio/sentry/protocol/Device;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V + public fun setProfile (Lio/sentry/ProfileContext;)V public fun setResponse (Lio/sentry/protocol/Response;)V public fun setRuntime (Lio/sentry/protocol/SentryRuntime;)V public fun setTrace (Lio/sentry/SpanContext;)V @@ -4623,6 +4712,7 @@ public final class io/sentry/protocol/DebugImage$JsonKeys { public final class io/sentry/protocol/DebugMeta : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public static fun buildDebugMeta (Lio/sentry/protocol/DebugMeta;Lio/sentry/SentryOptions;)Lio/sentry/protocol/DebugMeta; public fun getImages ()Ljava/util/List; public fun getSdkInfo ()Lio/sentry/protocol/SdkInfo; public fun getUnknown ()Ljava/util/Map; diff --git a/sentry/src/main/java/io/sentry/CpuCollectionData.java b/sentry/src/main/java/io/sentry/CpuCollectionData.java index 081063a53f..cf011a1e5c 100644 --- a/sentry/src/main/java/io/sentry/CpuCollectionData.java +++ b/sentry/src/main/java/io/sentry/CpuCollectionData.java @@ -1,15 +1,25 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class CpuCollectionData { final long timestampMillis; final double cpuUsagePercentage; + final @NotNull SentryDate timestamp; - public CpuCollectionData(final long timestampMillis, final double cpuUsagePercentage) { + public CpuCollectionData( + final long timestampMillis, + final double cpuUsagePercentage, + final @NotNull SentryDate timestamp) { this.timestampMillis = timestampMillis; this.cpuUsagePercentage = cpuUsagePercentage; + this.timestamp = timestamp; + } + + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getTimestampMillis() { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 81ae409286..228a3c8738 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -278,6 +278,12 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + return Sentry.getCurrentScopes().captureProfileChunk(profilingContinuousData); + } + @Deprecated @Override public @Nullable SentryTraceHeader traceHeaders() { diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index f1b5c31a6c..4bcc431246 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -266,6 +266,11 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return scopes.captureTransaction(transaction, traceContext, hint, profilingTraceData); } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return scopes.captureProfileChunk(profileChunk); + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index f85f2b7c4a..cfb29ca09e 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -528,6 +528,16 @@ default SentryId captureTransaction(@NotNull SentryTransaction transaction, @Nul return captureTransaction(transaction, traceContext, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profileChunk the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk); + /** * Creates a Transaction and returns the instance. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 8d1815b4c8..f9772a79cf 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -277,6 +277,17 @@ SentryId captureTransaction( return captureTransaction(transaction, null, null, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profilingContinuousData the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData, final @Nullable IScope scope); + @NotNull @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint); diff --git a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java index cdde808ba5..603c452364 100644 --- a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java +++ b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java @@ -15,7 +15,8 @@ public void setup() {} public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { final long now = System.currentTimeMillis(); final long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(now, usedMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 6c46306cc7..181a8d7638 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -91,6 +91,8 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(Message.class, new Message.Deserializer()); deserializersByClass.put(MetricSummary.class, new MetricSummary.Deserializer()); deserializersByClass.put(OperatingSystem.class, new OperatingSystem.Deserializer()); + deserializersByClass.put(ProfileChunk.class, new ProfileChunk.Deserializer()); + deserializersByClass.put(ProfileContext.class, new ProfileContext.Deserializer()); deserializersByClass.put(ProfilingTraceData.class, new ProfilingTraceData.Deserializer()); deserializersByClass.put( ProfilingTransactionData.class, new ProfilingTransactionData.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index 78658091de..2e23cc7b8f 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -2,7 +2,6 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.Cached; -import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryTransaction; @@ -65,34 +64,8 @@ public MainEventProcessor(final @NotNull SentryOptions options) { } private void setDebugMeta(final @NotNull SentryBaseEvent event) { - final @NotNull List debugImages = new ArrayList<>(); - - if (options.getProguardUuid() != null) { - final DebugImage proguardMappingImage = new DebugImage(); - proguardMappingImage.setType(DebugImage.PROGUARD); - proguardMappingImage.setUuid(options.getProguardUuid()); - debugImages.add(proguardMappingImage); - } - - for (final @NotNull String bundleId : options.getBundleIds()) { - final DebugImage sourceBundleImage = new DebugImage(); - sourceBundleImage.setType(DebugImage.JVM); - sourceBundleImage.setDebugId(bundleId); - debugImages.add(sourceBundleImage); - } - - if (!debugImages.isEmpty()) { - DebugMeta debugMeta = event.getDebugMeta(); - - if (debugMeta == null) { - debugMeta = new DebugMeta(); - } - if (debugMeta.getImages() == null) { - debugMeta.setImages(debugImages); - } else { - debugMeta.getImages().addAll(debugImages); - } - + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(event.getDebugMeta(), options); + if (debugMeta != null) { event.setDebugMeta(debugMeta); } } diff --git a/sentry/src/main/java/io/sentry/MemoryCollectionData.java b/sentry/src/main/java/io/sentry/MemoryCollectionData.java index 0fbb66412e..6a85fa28b0 100644 --- a/sentry/src/main/java/io/sentry/MemoryCollectionData.java +++ b/sentry/src/main/java/io/sentry/MemoryCollectionData.java @@ -1,22 +1,33 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class MemoryCollectionData { final long timestampMillis; final long usedHeapMemory; final long usedNativeMemory; + final @NotNull SentryDate timestamp; public MemoryCollectionData( - final long timestampMillis, final long usedHeapMemory, final long usedNativeMemory) { + final long timestampMillis, + final long usedHeapMemory, + final long usedNativeMemory, + final @NotNull SentryDate timestamp) { this.timestampMillis = timestampMillis; this.usedHeapMemory = usedHeapMemory; this.usedNativeMemory = usedNativeMemory; + this.timestamp = timestamp; } - public MemoryCollectionData(final long timestampMillis, final long usedHeapMemory) { - this(timestampMillis, usedHeapMemory, -1); + public MemoryCollectionData( + final long timestampMillis, final long usedHeapMemory, final @NotNull SentryDate timestamp) { + this(timestampMillis, usedHeapMemory, -1, timestamp); + } + + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getTimestampMillis() { diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index c71fcd628c..314835b5d1 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -235,6 +235,11 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index fb8372d832..35cace8bdd 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -230,6 +230,11 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index f00f309544..5b4b67c0bd 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -59,6 +59,12 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + return SentryId.EMPTY_ID; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java new file mode 100644 index 0000000000..44cb921209 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -0,0 +1,317 @@ +package io.sentry; + +import io.sentry.profilemeasurements.ProfileMeasurement; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class ProfileChunk implements JsonUnknown, JsonSerializable { + private @Nullable DebugMeta debugMeta; + private @NotNull SentryId profilerId; + private @NotNull SentryId chunkId; + private @Nullable SdkVersion clientSdk; + private final @NotNull Map measurements; + private @NotNull String platform; + private @NotNull String release; + private @Nullable String environment; + private @NotNull String version; + + private final @NotNull File traceFile; + + /** Profile trace encoded with Base64. */ + private @Nullable String sampledProfile = null; + + private @Nullable Map unknown; + + public ProfileChunk() { + this( + SentryId.EMPTY_ID, + SentryId.EMPTY_ID, + new File("dummy"), + new HashMap<>(), + SentryOptions.empty()); + } + + public ProfileChunk( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull SentryOptions options) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.traceFile = traceFile; + this.measurements = measurements; + this.debugMeta = null; + this.clientSdk = options.getSdkVersion(); + this.release = options.getRelease() != null ? options.getRelease() : ""; + this.environment = options.getEnvironment(); + this.platform = "android"; + this.version = "2"; + } + + public @NotNull Map getMeasurements() { + return measurements; + } + + public @Nullable DebugMeta getDebugMeta() { + return debugMeta; + } + + public void setDebugMeta(final @Nullable DebugMeta debugMeta) { + this.debugMeta = debugMeta; + } + + public @Nullable SdkVersion getClientSdk() { + return clientSdk; + } + + public @NotNull SentryId getChunkId() { + return chunkId; + } + + public @Nullable String getEnvironment() { + return environment; + } + + public @NotNull String getPlatform() { + return platform; + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + public @NotNull String getRelease() { + return release; + } + + public @Nullable String getSampledProfile() { + return sampledProfile; + } + + public void setSampledProfile(final @Nullable String sampledProfile) { + this.sampledProfile = sampledProfile; + } + + public @NotNull File getTraceFile() { + return traceFile; + } + + public @NotNull String getVersion() { + return version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileChunk)) return false; + ProfileChunk that = (ProfileChunk) o; + return Objects.equals(debugMeta, that.debugMeta) + && Objects.equals(profilerId, that.profilerId) + && Objects.equals(chunkId, that.chunkId) + && Objects.equals(clientSdk, that.clientSdk) + && Objects.equals(measurements, that.measurements) + && Objects.equals(platform, that.platform) + && Objects.equals(release, that.release) + && Objects.equals(environment, that.environment) + && Objects.equals(version, that.version) + && Objects.equals(sampledProfile, that.sampledProfile) + && Objects.equals(unknown, that.unknown); + } + + @Override + public int hashCode() { + return Objects.hash( + debugMeta, + profilerId, + chunkId, + clientSdk, + measurements, + platform, + release, + environment, + version, + sampledProfile, + unknown); + } + + public static final class Builder { + private final @NotNull SentryId profilerId; + private final @NotNull SentryId chunkId; + private final @NotNull Map measurements; + private final @NotNull File traceFile; + + public Builder( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull Map measurements, + final @NotNull File traceFile) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.measurements = measurements; + this.traceFile = traceFile; + } + + public ProfileChunk build(SentryOptions options) { + return new ProfileChunk(profilerId, chunkId, traceFile, measurements, options); + } + } + + // JsonSerializable + + public static final class JsonKeys { + public static final String DEBUG_META = "debug_meta"; + public static final String PROFILER_ID = "profiler_id"; + public static final String CHUNK_ID = "chunk_id"; + public static final String CLIENT_SDK = "client_sdk"; + public static final String MEASUREMENTS = "measurements"; + public static final String PLATFORM = "platform"; + public static final String RELEASE = "release"; + public static final String ENVIRONMENT = "environment"; + public static final String VERSION = "version"; + public static final String SAMPLED_PROFILE = "sampled_profile"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + if (debugMeta != null) { + writer.name(JsonKeys.DEBUG_META).value(logger, debugMeta); + } + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + writer.name(JsonKeys.CHUNK_ID).value(logger, chunkId); + if (clientSdk != null) { + writer.name(JsonKeys.CLIENT_SDK).value(logger, clientSdk); + } + if (!measurements.isEmpty()) { + writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); + } + writer.name(JsonKeys.PLATFORM).value(logger, platform); + writer.name(JsonKeys.RELEASE).value(logger, release); + if (environment != null) { + writer.name(JsonKeys.ENVIRONMENT).value(logger, environment); + } + writer.name(JsonKeys.VERSION).value(logger, version); + if (sampledProfile != null) { + writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull ProfileChunk deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + ProfileChunk data = new ProfileChunk(); + Map unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.DEBUG_META: + DebugMeta debugMeta = reader.nextOrNull(logger, new DebugMeta.Deserializer()); + if (debugMeta != null) { + data.debugMeta = debugMeta; + } + break; + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + case JsonKeys.CHUNK_ID: + SentryId chunkId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (chunkId != null) { + data.chunkId = chunkId; + } + break; + case JsonKeys.CLIENT_SDK: + SdkVersion clientSdk = reader.nextOrNull(logger, new SdkVersion.Deserializer()); + if (clientSdk != null) { + data.clientSdk = clientSdk; + } + break; + case JsonKeys.MEASUREMENTS: + Map measurements = + reader.nextMapOrNull(logger, new ProfileMeasurement.Deserializer()); + if (measurements != null) { + data.measurements.putAll(measurements); + } + break; + case JsonKeys.PLATFORM: + String platform = reader.nextStringOrNull(); + if (platform != null) { + data.platform = platform; + } + break; + case JsonKeys.RELEASE: + String release = reader.nextStringOrNull(); + if (release != null) { + data.release = release; + } + break; + case JsonKeys.ENVIRONMENT: + String environment = reader.nextStringOrNull(); + if (environment != null) { + data.environment = environment; + } + break; + case JsonKeys.VERSION: + String version = reader.nextStringOrNull(); + if (version != null) { + data.version = version; + } + break; + case JsonKeys.SAMPLED_PROFILE: + String sampledProfile = reader.nextStringOrNull(); + if (sampledProfile != null) { + data.sampledProfile = sampledProfile; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/ProfileContext.java b/sentry/src/main/java/io/sentry/ProfileContext.java new file mode 100644 index 0000000000..3a7fa9af2f --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileContext.java @@ -0,0 +1,122 @@ +package io.sentry; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.protocol.SentryId; +import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Open +public class ProfileContext implements JsonUnknown, JsonSerializable { + public static final String TYPE = "profile"; + + /** Determines which trace the Span belongs to. */ + private @NotNull SentryId profilerId; + + private @Nullable Map unknown; + + public ProfileContext() { + this(SentryId.EMPTY_ID); + } + + public ProfileContext(final @NotNull SentryId profilerId) { + this.profilerId = profilerId; + } + + /** + * Copy constructor. + * + * @param profileContext the ProfileContext to copy + */ + public ProfileContext(final @NotNull ProfileContext profileContext) { + this.profilerId = profileContext.profilerId; + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(profileContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileContext)) return false; + ProfileContext that = (ProfileContext) o; + return profilerId.equals(that.profilerId); + } + + @Override + public int hashCode() { + return Objects.hash(profilerId); + } + + // region JsonSerializable + + public static final class JsonKeys { + public static final String PROFILER_ID = "profiler_id"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull ProfileContext deserialize( + @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + ProfileContext data = new ProfileContext(); + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index d7e916c09e..a8138a1a7c 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -816,6 +816,35 @@ public void flush(long timeoutMillis) { return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + Objects.requireNonNull(profilingContinuousData, "profilingContinuousData is required"); + + @NotNull SentryId sentryId = SentryId.EMPTY_ID; + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureTransaction' call is a no-op."); + } else { + try { + sentryId = getClient().captureProfileChunk(profilingContinuousData, getScope()); + } catch (Throwable e) { + getOptions() + .getLogger() + .log( + SentryLevel.ERROR, + "Error while capturing profile chunk with id: " + + profilingContinuousData.getChunkId(), + e); + } + } + return sentryId; + } + @Override public @NotNull ITransaction startTransaction( final @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 436b266e8d..fc4266571c 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -269,6 +269,11 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { .captureTransaction(transaction, traceContext, hint, profilingTraceData); } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return Sentry.getCurrentScopes().captureProfileChunk(profileChunk); + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index cdb57b7d7f..7c91523c15 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -10,6 +10,7 @@ import io.sentry.metrics.IMetricsClient; import io.sentry.metrics.NoopMetricsAggregator; import io.sentry.protocol.Contexts; +import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -475,8 +476,7 @@ private SentryEvent processEvent( return event; } - @Nullable - private SentryTransaction processTransaction( + private @Nullable SentryTransaction processTransaction( @NotNull SentryTransaction transaction, final @NotNull Hint hint, final @NotNull List eventProcessors) { @@ -865,6 +865,42 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + Objects.requireNonNull(profileChunk, "profileChunk is required."); + + options + .getLogger() + .log(SentryLevel.DEBUG, "Capturing profile chunk: %s", profileChunk.getChunkId()); + + @NotNull SentryId sentryId = profileChunk.getChunkId(); + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(profileChunk.getDebugMeta(), options); + if (debugMeta != null) { + profileChunk.setDebugMeta(debugMeta); + } + + // BeforeSend and EventProcessors are not supported at the moment for Profile Chunks + + try { + final @NotNull SentryEnvelope envelope = + new SentryEnvelope( + new SentryEnvelopeHeader(sentryId, options.getSdkVersion(), null), + Collections.singletonList( + SentryEnvelopeItem.fromProfileChunk(profileChunk, options.getSerializer()))); + sentryId = sendEnvelope(envelope, null); + } catch (IOException | SentryEnvelopeException e) { + options + .getLogger() + .log(SentryLevel.WARNING, e, "Capturing profile chunk %s failed.", sentryId); + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; + } + + return sentryId; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 7862c8d664..11e397d791 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -35,6 +35,9 @@ @ApiStatus.Internal public final class SentryEnvelopeItem { + // Profiles bigger than 50 MB will be dropped by the backend, so we drop bigger ones + private static final long MAX_PROFILE_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -278,13 +281,64 @@ private static void ensureAttachmentSizeLimit( } } + public static @NotNull SentryEnvelopeItem fromProfileChunk( + final @NotNull ProfileChunk profileChunk, final @NotNull ISerializer serializer) + throws SentryEnvelopeException { + + final @NotNull File traceFile = profileChunk.getTraceFile(); + // Using CachedItem, so we read the trace file in the background + final CachedItem cachedItem = + new CachedItem( + () -> { + if (!traceFile.exists()) { + throw new SentryEnvelopeException( + String.format( + "Dropping profile chunk, because the file '%s' doesn't exists", + traceFile.getName())); + } + // The payload of the profile item is a json including the trace file encoded with + // base64 + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + if (base64Trace.isEmpty()) { + throw new SentryEnvelopeException("Profiling trace file is empty"); + } + profileChunk.setSampledProfile(base64Trace); + + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(profileChunk, writer); + return stream.toByteArray(); + } catch (IOException e) { + throw new SentryEnvelopeException( + String.format("Failed to serialize profile chunk\n%s", e.getMessage())); + } finally { + // In any case we delete the trace file + traceFile.delete(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.ProfileChunk, + () -> cachedItem.getBytes().length, + "application-json", + traceFile.getName()); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + public static @NotNull SentryEnvelopeItem fromProfilingTrace( final @NotNull ProfilingTraceData profilingTraceData, final long maxTraceFileSize, final @NotNull ISerializer serializer) throws SentryEnvelopeException { - File traceFile = profilingTraceData.getTraceFile(); + final @NotNull File traceFile = profilingTraceData.getTraceFile(); // Using CachedItem, so we read the trace file in the background final CachedItem cachedItem = new CachedItem( @@ -297,8 +351,10 @@ private static void ensureAttachmentSizeLimit( } // The payload of the profile item is a json including the trace file encoded with // base64 - byte[] traceFileBytes = readBytesFromFile(traceFile.getPath(), maxTraceFileSize); - String base64Trace = Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), maxTraceFileSize); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); if (base64Trace.isEmpty()) { throw new SentryEnvelopeException("Profiling trace file is empty"); } diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index f37b972454..595407036e 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -15,6 +15,7 @@ public enum SentryItemType implements JsonSerializable { Attachment("attachment"), Transaction("transaction"), Profile("profile"), + ProfileChunk("profile_chunk"), ClientReport("client_report"), ReplayEvent("replay_event"), ReplayRecording("replay_recording"), diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 9bfd540899..b5315df018 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -109,10 +109,16 @@ public SpanContext(final @NotNull SpanContext spanContext) { this.op = spanContext.op; this.description = spanContext.description; this.status = spanContext.status; + this.origin = spanContext.origin; final Map copiedTags = CollectionUtils.newConcurrentHashMap(spanContext.tags); if (copiedTags != null) { this.tags = copiedTags; } + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(spanContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } } public void setOperation(final @NotNull String operation) { diff --git a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java index b0cebf5439..12972d36e4 100644 --- a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java +++ b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java @@ -1,14 +1,20 @@ package io.sentry.profilemeasurements; +import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.JsonDeserializer; import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryDate; +import io.sentry.SentryNanotimeDate; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus; @@ -19,16 +25,26 @@ public final class ProfileMeasurementValue implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; + private @Nullable Double timestamp; private @NotNull String relativeStartNs; // timestamp in nanoseconds this frame was started private double value; // frame duration in nanoseconds + @SuppressWarnings("JavaUtilDate") public ProfileMeasurementValue() { - this(0L, 0); + this(0L, 0, new SentryNanotimeDate(new Date(0), 0)); } - public ProfileMeasurementValue(final @NotNull Long relativeStartNs, final @NotNull Number value) { + public ProfileMeasurementValue( + final @NotNull Long relativeStartNs, + final @NotNull Number value, + final @NotNull SentryDate timestamp) { this.relativeStartNs = relativeStartNs.toString(); this.value = value.doubleValue(); + this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); + } + + public @Nullable Double getTimestamp() { + return timestamp; } public double getValue() { @@ -46,7 +62,8 @@ public boolean equals(Object o) { ProfileMeasurementValue that = (ProfileMeasurementValue) o; return Objects.equals(unknown, that.unknown) && relativeStartNs.equals(that.relativeStartNs) - && value == that.value; + && value == that.value + && Objects.equals(timestamp, that.timestamp); } @Override @@ -59,6 +76,7 @@ public int hashCode() { public static final class JsonKeys { public static final String VALUE = "value"; public static final String START_NS = "elapsed_since_start_ns"; + public static final String TIMESTAMP = "timestamp"; } @Override @@ -67,6 +85,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.beginObject(); writer.name(JsonKeys.VALUE).value(logger, value); writer.name(JsonKeys.START_NS).value(logger, relativeStartNs); + if (timestamp != null) { + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -77,6 +98,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.endObject(); } + private @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) { + return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); + } + @Nullable @Override public Map getUnknown() { @@ -112,6 +137,18 @@ public static final class Deserializer implements JsonDeserializer(); diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index ba49e91519..4871ba44a0 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -6,6 +6,7 @@ import io.sentry.JsonSerializable; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.ProfileContext; import io.sentry.SpanContext; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -52,6 +53,8 @@ public Contexts(final @NotNull Contexts contexts) { this.setGpu(new Gpu((Gpu) value)); } else if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { this.setTrace(new SpanContext((SpanContext) value)); + } else if (ProfileContext.TYPE.equals(entry.getKey()) && value instanceof ProfileContext) { + this.setProfile(new ProfileContext((ProfileContext) value)); } else if (Response.TYPE.equals(entry.getKey()) && value instanceof Response) { this.setResponse(new Response((Response) value)); } else { @@ -75,6 +78,15 @@ public void setTrace(final @Nullable SpanContext traceContext) { this.put(SpanContext.TYPE, traceContext); } + public @Nullable ProfileContext getProfile() { + return toContextType(ProfileContext.TYPE, ProfileContext.class); + } + + public void setProfile(final @Nullable ProfileContext profileContext) { + Objects.requireNonNull(profileContext, "profileContext is required"); + this.put(ProfileContext.TYPE, profileContext); + } + public @Nullable App getApp() { return toContextType(App.TYPE, App.class); } @@ -261,6 +273,9 @@ public static final class Deserializer implements JsonDeserializer { case SpanContext.TYPE: contexts.setTrace(new SpanContext.Deserializer().deserialize(reader, logger)); break; + case ProfileContext.TYPE: + contexts.setProfile(new ProfileContext.Deserializer().deserialize(reader, logger)); + break; case Response.TYPE: contexts.setResponse(new Response.Deserializer().deserialize(reader, logger)); break; diff --git a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java index 458c4de631..85ce67ab8e 100644 --- a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java +++ b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java @@ -6,12 +6,14 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryOptions; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,42 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) { this.sdkInfo = sdkInfo; } + @ApiStatus.Internal + public static @Nullable DebugMeta buildDebugMeta( + final @Nullable DebugMeta eventDebugMeta, final @NotNull SentryOptions options) { + final @NotNull List debugImages = new ArrayList<>(); + + if (options.getProguardUuid() != null) { + final DebugImage proguardMappingImage = new DebugImage(); + proguardMappingImage.setType(DebugImage.PROGUARD); + proguardMappingImage.setUuid(options.getProguardUuid()); + debugImages.add(proguardMappingImage); + } + + for (final @NotNull String bundleId : options.getBundleIds()) { + final DebugImage sourceBundleImage = new DebugImage(); + sourceBundleImage.setType(DebugImage.JVM); + sourceBundleImage.setDebugId(bundleId); + debugImages.add(sourceBundleImage); + } + + if (!debugImages.isEmpty()) { + DebugMeta debugMeta = eventDebugMeta; + + if (debugMeta == null) { + debugMeta = new DebugMeta(); + } + if (debugMeta.getImages() == null) { + debugMeta.setImages(debugImages); + } else { + debugMeta.getImages().addAll(debugImages); + } + + return debugMeta; + } + return null; + } + // JsonKeys public static final class JsonKeys { diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 9c6ff6ddf1..579dc24866 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -215,6 +215,12 @@ class HubAdapterTest { verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } + @Test fun `captureProfileChunk calls Hub`() { + val profileChunk = mock() + HubAdapter.getInstance().captureProfileChunk(profileChunk) + verify(scopes).captureProfileChunk(eq(profileChunk)) + } + @Test fun `startTransaction calls Hub`() { val transactionContext = mock() val samplingContext = mock() diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index e2914edff6..5a41c8facd 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -24,5 +24,6 @@ class JavaMemoryCollectorTest { assertEquals(-1, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 470a440f1c..ba6fea8c14 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -28,8 +28,11 @@ import java.io.OutputStream import java.io.OutputStreamWriter import java.io.StringReader import java.io.StringWriter +import java.math.BigDecimal +import java.math.RoundingMode import java.nio.file.Files import java.util.Date +import java.util.HashMap import java.util.TimeZone import java.util.UUID import kotlin.test.BeforeTest @@ -489,10 +492,29 @@ class JsonSerializerTest { } } + @Test + fun `serializes profile context`() { + val profileContext = ProfileContext(SentryId("3367f5196c494acaae85bbbd535379ac")) + val expected = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val json = serializeToString(profileContext) + assertEquals(expected, json) + } + + @Test + fun `deserializes profile context`() { + val json = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val actual = fixture.serializer.deserialize(StringReader(json), ProfileContext::class.java) + assertNotNull(actual) { + assertEquals(SentryId("3367f5196c494acaae85bbbd535379ac"), it.profilerId) + } + } + @Test fun `serializes profilingTraceData`() { val profilingTraceData = ProfilingTraceData(fixture.traceFile, NoOpTransaction.getInstance()) val now = Date() + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN).toDouble() profilingTraceData.androidApiLevel = 21 profilingTraceData.deviceLocale = "deviceLocale" profilingTraceData.deviceManufacturer = "deviceManufacturer" @@ -522,22 +544,22 @@ class JsonSerializerTest { ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(2, 100.52)) + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 104.52)) + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 10.52)) + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) ) ) ) @@ -593,7 +615,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 60.1, - "elapsed_since_start_ns" to "1" + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds ) ) ), @@ -603,7 +626,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 100.52, - "elapsed_since_start_ns" to "2" + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds ) ) ), @@ -613,7 +637,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 104.52, - "elapsed_since_start_ns" to "3" + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds ) ) ), @@ -623,7 +648,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 10.52, - "elapsed_since_start_ns" to "5" + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds ) ) ) @@ -754,23 +780,23 @@ class JsonSerializerTest { val expectedMeasurements = mapOf( ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, mock())) ), ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( ProfileMeasurement.UNIT_NANOSECONDS, - listOf(ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(2, 100, mock())) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 1000)) + listOf(ProfileMeasurementValue(3, 1000, mock())) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(4, 1100)) + listOf(ProfileMeasurementValue(4, 1100, mock())) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 17.04)) + listOf(ProfileMeasurementValue(5, 17.04, mock())) ) ) assertEquals(expectedMeasurements, profilingTraceData.measurementsMap) @@ -787,10 +813,11 @@ class JsonSerializerTest { @Test fun `serializes profileMeasurement`() { - val measurementValues = listOf(ProfileMeasurementValue(1, 2), ProfileMeasurementValue(3, 4)) + val now = SentryNanotimeDate(Date(1), 1) + val measurementValues = listOf(ProfileMeasurementValue(1, 2, now), ProfileMeasurementValue(3, 4, now)) val profileMeasurement = ProfileMeasurement(ProfileMeasurement.UNIT_NANOSECONDS, measurementValues) val actual = serializeToString(profileMeasurement) - val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\"}]}" + val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\",\"timestamp\":0.001000}]}" assertEquals(expected, actual) } @@ -799,22 +826,22 @@ class JsonSerializerTest { val json = """{ "unit":"hz", "values":[ - {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2"} + {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2", "timestamp": 0.001} ] }""" val profileMeasurement = fixture.serializer.deserialize(StringReader(json), ProfileMeasurement::class.java) val expected = ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1), ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(0), 0)), ProfileMeasurementValue(2, 100, SentryNanotimeDate(Date(1), 1))) ) assertEquals(expected, profileMeasurement) } @Test fun `serializes profileMeasurementValue`() { - val profileMeasurementValue = ProfileMeasurementValue(1, 2) + val profileMeasurementValue = ProfileMeasurementValue(1, 2, SentryNanotimeDate(Date(1), 1)) val actual = serializeToString(profileMeasurementValue) - val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"}" + val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000}" assertEquals(expected, actual) } @@ -822,10 +849,205 @@ class JsonSerializerTest { fun `deserializes profileMeasurementValue`() { val json = """{"value":"60.1","elapsed_since_start_ns":"1"}""" val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) - val expected = ProfileMeasurementValue(1, 60.1) + val expected = ProfileMeasurementValue(1, 60.1, mock()) assertEquals(expected, profileMeasurementValue) assertEquals(60.1, profileMeasurementValue?.value) assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.0, profileMeasurementValue?.timestamp) + } + + @Test + fun `deserializes profileMeasurementValue with timestamp`() { + val json = """{"value":"60.1","elapsed_since_start_ns":"1","timestamp":0.001000}""" + val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) + val expected = ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(1), 1)) + assertEquals(expected, profileMeasurementValue) + assertEquals(60.1, profileMeasurementValue?.value) + assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.001, profileMeasurementValue?.timestamp) + } + + @Test + fun `serializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + fixture.options.sdkVersion = SdkVersion("test", "1.2.3") + fixture.options.release = "release" + fixture.options.environment = "environment" + val profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), fixture.options) + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = + BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN) + .toDouble() + profileChunk.sampledProfile = "sampled profile in base 64" + profileChunk.measurements.putAll( + hashMapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) + ), + ProfileMeasurement.ID_CPU_USAGE to + ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) + ) + ) + ) + + val actual = serializeToString(profileChunk) + val reader = StringReader(actual) + val objectReader = JsonObjectReader(reader) + val element = JsonObjectDeserializer().deserialize(objectReader) as Map<*, *> + + assertEquals("android", element["platform"] as String) + assertEquals(profilerId.toString(), element["profiler_id"] as String) + assertEquals(chunkId.toString(), element["chunk_id"] as String) + assertEquals("environment", element["environment"] as String) + assertEquals("release", element["release"] as String) + assertEquals(mapOf("name" to "test", "version" to "1.2.3"), element["client_sdk"] as Map) + assertEquals("2", element["version"] as String) + assertEquals("sampled profile in base 64", element["sampled_profile"] as String) + assertEquals( + mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + mapOf( + "unit" to ProfileMeasurement.UNIT_HZ, + "values" to listOf( + mapOf( + "value" to 60.1, + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 100.52, + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 104.52, + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_CPU_USAGE to + mapOf( + "unit" to ProfileMeasurement.UNIT_PERCENT, + "values" to listOf( + mapOf( + "value" to 10.52, + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds + ) + ) + ) + ), + element["measurements"] + ) + } + + @Test + fun `deserializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + val json = """{ + "client_sdk":{"name":"test","version":"1.2.3"}, + "chunk_id":"$chunkId", + "environment":"environment", + "platform":"android", + "profiler_id":"$profilerId", + "release":"release", + "sampled_profile":"sampled profile in base 64", + "version":"2", + "measurements":{ + "screen_frame_rates": { + "unit":"hz", + "values":[ + {"value":"60.1","elapsed_since_start_ns":"1"} + ] + }, + "frozen_frame_renders": { + "unit":"nanosecond", + "values":[ + {"value":"100","elapsed_since_start_ns":"2"} + ] + }, + "memory_footprint": { + "unit":"byte", + "values":[ + {"value":"1000","elapsed_since_start_ns":"3"} + ] + }, + "memory_native_footprint": { + "unit":"byte", + "values":[ + {"value":"1100","elapsed_since_start_ns":"4"} + ] + }, + "cpu_usage": { + "unit":"percent", + "values":[ + {"value":"17.04","elapsed_since_start_ns":"5"} + ] + } + } + }""" + val profileChunk = fixture.serializer.deserialize(StringReader(json), ProfileChunk::class.java) + assertNotNull(profileChunk) + assertEquals(SdkVersion("test", "1.2.3"), profileChunk.clientSdk) + assertEquals(chunkId, profileChunk.chunkId) + assertEquals("environment", profileChunk.environment) + assertEquals("android", profileChunk.platform) + assertEquals(profilerId, profileChunk.profilerId) + assertEquals("release", profileChunk.release) + assertEquals("sampled profile in base 64", profileChunk.sampledProfile) + assertEquals("2", profileChunk.version) + val expectedMeasurements = mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, mock())) + ), + ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( + ProfileMeasurement.UNIT_NANOSECONDS, + listOf(ProfileMeasurementValue(2, 100, mock())) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 1000, mock())) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(4, 1100, mock())) + ), + ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 17.04, mock())) + ) + ) + assertEquals(expectedMeasurements, profileChunk.measurements) } @Test diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index 94af1acc9f..513a5e51f8 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -32,6 +32,10 @@ class NoOpHubTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock())) + @Test fun `captureException returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureException(RuntimeException())) diff --git a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt index 919ce5f083..8f8d76eba2 100644 --- a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt @@ -63,6 +63,10 @@ class NoOpSentryClientTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock(), mock())) + @Test fun `captureCheckIn returns empty id`() { assertEquals(SentryId.EMPTY_ID, sut.captureCheckIn(mock(), mock(), mock())) diff --git a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt index e105e105c6..ad821b952c 100644 --- a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt +++ b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt @@ -1,5 +1,6 @@ package io.sentry +import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -16,8 +17,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple memory data is saved`() { val data = fixture.getSut() - val memData1 = MemoryCollectionData(0, 0, 0) - val memData2 = MemoryCollectionData(1, 1, 1) + val t1 = mock() + val t2 = mock() + val memData1 = MemoryCollectionData(0, 0, 0, t1) + val memData2 = MemoryCollectionData(1, 1, 1, t2) data.addMemoryData(memData1) data.addMemoryData(memData2) val savedMemoryData = data.memoryData @@ -28,8 +31,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple cpu data is saved`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) - val cpuData2 = CpuCollectionData(1, 1.0) + val t1 = mock() + val t2 = mock() + val cpuData1 = CpuCollectionData(0, 0.0, t1) + val cpuData2 = CpuCollectionData(1, 1.0, t2) data.addCpuData(cpuData1) data.addCpuData(cpuData2) val savedCpuData = data.cpuData @@ -40,7 +45,7 @@ class PerformanceCollectionDataTest { @Test fun `null values are ignored`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) + val cpuData1 = CpuCollectionData(0, 0.0, mock()) data.addCpuData(cpuData1) data.addCpuData(null) data.addMemoryData(null) diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index 19123a23ed..6c466d6570 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -215,6 +215,12 @@ class ScopesAdapterTest { verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } + @Test fun `captureProfileChunk calls Scopes`() { + val profileChunk = mock() + ScopesAdapter.getInstance().captureProfileChunk(profileChunk) + verify(scopes).captureProfileChunk(eq(profileChunk)) + } + @Test fun `startTransaction calls Scopes`() { val transactionContext = mock() val samplingContext = mock() diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 958592cdb8..7053c422aa 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1581,6 +1581,52 @@ class ScopesTest { } //endregion + //region captureProfileChunk tests + @Test + fun `when captureProfileChunk is called on disabled client, do nothing`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = createScopes(options) + val mockClient = mock() + sut.bindClient(mockClient) + sut.close() + + sut.captureProfileChunk(mock()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + } + + @Test + fun `when captureProfileChunk, captureProfileChunk on the client should be called`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = createScopes(options) + val mockClient = createSentryClientMock() + sut.bindClient(mockClient) + + val profileChunk = mock() + sut.captureProfileChunk(profileChunk) + verify(mockClient).captureProfileChunk(eq(profileChunk), any()) + } + + @Test + fun `when profileChunk is called, lastEventId is not set`() { + val options = SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + setSerializer(mock()) + } + val sut = createScopes(options) + val mockClient = createSentryClientMock() + sut.bindClient(mockClient) + sut.captureProfileChunk(mock()) + assertEquals(SentryId.EMPTY_ID, sut.lastEventId) + } + //endregion + //region profiling tests @Test diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 88a6a1adc6..a46f37c1bb 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -42,6 +42,7 @@ import org.mockito.kotlin.mockingDetails import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.msgpack.core.MessagePack @@ -78,6 +79,8 @@ class SentryClientTest { val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() val scopes = mock() val sentryTracer: SentryTracer + val profileChunk: ProfileChunk + val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var sentryOptions: SentryOptions = SentryOptions().apply { dsn = dsnString @@ -97,12 +100,12 @@ class SentryClientTest { whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), scopes) sentryTracer.startChild("a-span", "span 1").finish() + profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), sentryOptions) } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) var attachment2 = Attachment("hello2".toByteArray(), "hello2.txt", "text/plain", true) var attachment3 = Attachment("hello3".toByteArray(), "hello3.txt", "text/plain", true) - val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var profilingTraceData = ProfilingTraceData(profilingTraceFile, sentryTracer) var profilingNonExistingTraceData = ProfilingTraceData(File("non_existent.trace"), sentryTracer) @@ -1085,6 +1088,22 @@ class SentryClientTest { ) } + @Test + fun `captureProfileChunk ignores beforeSend`() { + var invoked = false + fixture.sentryOptions.setBeforeSendTransaction { t, _ -> invoked = true; t } + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + assertFalse(invoked) + } + + @Test + fun `captureProfileChunk ignores Event Processors`() { + val mockProcessor = mock() + fixture.sentryOptions.addEventProcessor(mockProcessor) + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + verifyNoInteractions(mockProcessor) + } + @Test fun `when captureSession and no release is set, do nothing`() { fixture.getSut().captureSession(createSession("")) @@ -1485,6 +1504,29 @@ class SentryClientTest { assertFails { verifyProfilingTraceInEnvelope(SentryId(fixture.profilingNonExistingTraceData.profileId)) } } + @Test + fun `when captureProfileChunk`() { + val client = fixture.getSut() + client.captureProfileChunk(fixture.profileChunk, mock()) + verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId) + } + + @Test + fun `when captureProfileChunk with empty trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.writeText("") + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + + @Test + fun `when captureProfileChunk with non existing profiling trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.delete() + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + @Test fun `when captureTransaction with attachments not added to transaction`() { val transaction = SentryTransaction(fixture.sentryTracer) @@ -3025,6 +3067,19 @@ class SentryClientTest { ) } + private fun verifyProfileChunkInEnvelope(eventId: SentryId?) { + verify(fixture.transport).send( + check { actual -> + assertEquals(eventId, actual.header.eventId) + + val profilingTraceItem = actual.items.firstOrNull { item -> + item.header.type == SentryItemType.ProfileChunk + } + assertNotNull(profilingTraceItem?.data) + } + ) + } + private class AbnormalHint(private val mechanism: String? = null) : AbnormalExit { override fun mechanism(): String? = mechanism override fun ignoreCurrentThread(): Boolean = false diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index 760d1270e5..a85e940e22 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -462,6 +462,94 @@ class SentryEnvelopeItemTest { ) } + @Test + fun `fromProfileChunk saves file as Base64`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + verify(profileChunk).sampledProfile = + Base64.encodeToString(fixture.bytes, Base64.NO_WRAP or Base64.NO_PADDING) + } + + @Test + fun `fromProfileChunk deletes file only after reading data`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + assert(file.exists()) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assert(file.exists()) + chunk.data + assertFalse(file.exists()) + } + + @Test + fun `fromProfileChunk with invalid file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with unreadable file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + file.writeBytes(fixture.bytes) + file.setReadable(false) + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with empty file throws`() { + val file = File(fixture.pathname) + file.writeBytes(ByteArray(0)) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assertFailsWith("Profiling trace file is empty") { + chunk.data + } + } + + @Test + fun `fromProfileChunk with file too big`() { + val file = File(fixture.pathname) + val maxSize = 50 * 1024 * 1024 // 50MB + file.writeBytes(ByteArray((maxSize + 1)) { 0 }) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val exception = assertFailsWith { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + + assertEquals( + "Reading file failed, because size located at " + + "'${fixture.pathname}' with ${file.length()} bytes is bigger than the maximum " + + "allowed size of $maxSize bytes.", + exception.message + ) + } + @Test fun `fromReplay encodes payload into msgpack`() { val file = Files.createTempFile("replay", "").toFile() diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index c1fb47b1c7..88974c9556 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.ProfileContext import io.sentry.SpanContext import kotlin.test.Test import kotlin.test.assertEquals @@ -19,6 +20,7 @@ class ContextsTest { contexts.setGpu(Gpu()) contexts.setResponse(Response()) contexts.trace = SpanContext("op") + contexts.profile = ProfileContext(SentryId()) val clone = Contexts(contexts) @@ -31,15 +33,18 @@ class ContextsTest { assertNotSame(contexts.runtime, clone.runtime) assertNotSame(contexts.gpu, clone.gpu) assertNotSame(contexts.trace, clone.trace) + assertNotSame(contexts.profile, clone.profile) assertNotSame(contexts.response, clone.response) } @Test fun `copying contexts will have the same values`() { val contexts = Contexts() + val id = SentryId() contexts["some-property"] = "some-value" contexts.trace = SpanContext("op") contexts.trace!!.description = "desc" + contexts.profile = ProfileContext(id) val clone = Contexts(contexts) @@ -47,5 +52,6 @@ class ContextsTest { assertNotSame(contexts, clone) assertEquals(contexts["some-property"], clone["some-property"]) assertEquals(contexts.trace!!.description, clone.trace!!.description) + assertEquals(contexts.profile!!.profilerId, clone.profile!!.profilerId) } } diff --git a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt index 17544a300f..21395dfc5c 100644 --- a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.SentryOptions import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -16,4 +17,75 @@ class DebugMetaTest { assertEquals(3, it.size) } } + + @Test + fun `when event does not have debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event does not have debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event has debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta as well as images and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta().also { it.images = listOf() }, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } } From 8222bdfe5aa5fd6a2f07f3e336ddc30e7a5b7bf4 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 11 Nov 2024 17:30:30 +0100 Subject: [PATCH 2/5] removed timestampMillis from MemoryCollectionData and CpuCollectionData, now it uses timestamp.nanotime() to achieve same result profiler resets chunk id and profiler id in case profile is not recorded due to an error --- .../android/core/AndroidContinuousProfiler.java | 3 +++ .../sentry/android/core/AndroidCpuCollector.java | 4 +--- .../android/core/AndroidMemoryCollector.java | 3 +-- .../io/sentry/android/core/AndroidProfiler.java | 6 +++--- .../android/core/AndroidCpuCollectorTest.kt | 1 - .../android/core/AndroidMemoryCollectorTest.kt | 1 - .../sentry/android/core/AndroidProfilerTest.kt | 6 +++--- .../core/AndroidTransactionProfilerTest.kt | 6 +++--- sentry/api/sentry.api | 6 ++---- .../main/java/io/sentry/CpuCollectionData.java | 11 +---------- .../main/java/io/sentry/JavaMemoryCollector.java | 3 +-- .../java/io/sentry/MemoryCollectionData.java | 16 +++------------- .../java/io/sentry/JavaMemoryCollectorTest.kt | 1 - .../io/sentry/PerformanceCollectionDataTest.kt | 10 +++++----- 14 files changed, 26 insertions(+), 51 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index b9cd9ca9da..cb851c4f5c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -152,6 +152,9 @@ private synchronized void stop(final boolean restartProfiler) { // check if profiler end successfully if (endData == null) { + // A problem occurred. Profile chunk is not captured. Let's reset ids. + chunkId = SentryId.EMPTY_ID; + profilerId = SentryId.EMPTY_ID; return; } 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 2c9ef38cbb..de2526db62 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 @@ -86,9 +86,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti CpuCollectionData cpuData = new CpuCollectionData( - System.currentTimeMillis(), - (cpuUsagePercentage / (double) numCores) * 100.0, - new SentryNanotimeDate()); + (cpuUsagePercentage / (double) numCores) * 100.0, new SentryNanotimeDate()); performanceCollectionData.addCpuData(cpuData); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java index 866a1db1a4..41c2d2da03 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java @@ -16,11 +16,10 @@ public void setup() {} @Override public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { - long now = System.currentTimeMillis(); long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize(); MemoryCollectionData memoryData = - new MemoryCollectionData(now, usedMemory, usedNativeMemory, new SentryNanotimeDate()); + new MemoryCollectionData(usedMemory, usedNativeMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } 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 75d507246c..ba2af21895 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 @@ -331,21 +331,21 @@ private void putPerformanceCollectionDataInMeasurements( if (cpuData != null) { cpuUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff, + cpuData.getTimestamp().nanoTimestamp() + timestampDiff, cpuData.getCpuUsagePercentage(), cpuData.getTimestamp())); } if (memoryData != null && memoryData.getUsedHeapMemory() > -1) { memoryUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, + memoryData.getTimestamp().nanoTimestamp() + timestampDiff, memoryData.getUsedHeapMemory(), memoryData.getTimestamp())); } if (memoryData != null && memoryData.getUsedNativeMemory() > -1) { nativeMemoryUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, + memoryData.getTimestamp().nanoTimestamp() + timestampDiff, memoryData.getUsedNativeMemory(), memoryData.getTimestamp())); } 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 5df457c700..a1ae2279e5 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 @@ -55,7 +55,6 @@ class AndroidCpuCollectorTest { val cpuData = data.cpuData assertNotNull(cpuData) assertNotEquals(0.0, cpuData.cpuUsagePercentage) - assertNotEquals(0, cpuData.timestampMillis) assertNotEquals(0, cpuData.timestamp.nanoTimestamp()) } 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 3f00775d69..be41874a83 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 @@ -27,7 +27,6 @@ class AndroidMemoryCollectorTest { assertNotEquals(-1, memoryData.usedNativeMemory) assertEquals(usedNativeMemory, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) - assertNotEquals(0, memoryData.timestampMillis) assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index e9d11decb0..cf12ecc03f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -281,12 +281,12 @@ class AndroidProfilerTest { var singleData = PerformanceCollectionData() val t1 = mock() val t2 = mock() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3, t1)) - singleData.addCpuData(CpuCollectionData(1, 1.4, t1)) + singleData.addMemoryData(MemoryCollectionData(2, 3, t1)) + singleData.addCpuData(CpuCollectionData(1.4, t1)) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4, t2)) + singleData.addMemoryData(MemoryCollectionData(3, 4, t2)) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 8208038496..572c90425c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -459,12 +459,12 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3, mock())) - singleData.addCpuData(CpuCollectionData(1, 1.4, mock())) + singleData.addMemoryData(MemoryCollectionData(2, 3, mock())) + singleData.addCpuData(CpuCollectionData(1.4, mock())) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4, mock())) + singleData.addMemoryData(MemoryCollectionData(3, 4, mock())) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1cc82cf17e..6eafb92dfb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -308,10 +308,9 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { } public final class io/sentry/CpuCollectionData { - public fun (JDLio/sentry/SentryDate;)V + public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D public fun getTimestamp ()Lio/sentry/SentryDate; - public fun getTimestampMillis ()J } public final class io/sentry/CustomSamplingContext { @@ -1270,10 +1269,9 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se } public final class io/sentry/MemoryCollectionData { - public fun (JJJLio/sentry/SentryDate;)V public fun (JJLio/sentry/SentryDate;)V + public fun (JLio/sentry/SentryDate;)V public fun getTimestamp ()Lio/sentry/SentryDate; - public fun getTimestampMillis ()J public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J } diff --git a/sentry/src/main/java/io/sentry/CpuCollectionData.java b/sentry/src/main/java/io/sentry/CpuCollectionData.java index cf011a1e5c..bcbab7c136 100644 --- a/sentry/src/main/java/io/sentry/CpuCollectionData.java +++ b/sentry/src/main/java/io/sentry/CpuCollectionData.java @@ -5,15 +5,10 @@ @ApiStatus.Internal public final class CpuCollectionData { - final long timestampMillis; final double cpuUsagePercentage; final @NotNull SentryDate timestamp; - public CpuCollectionData( - final long timestampMillis, - final double cpuUsagePercentage, - final @NotNull SentryDate timestamp) { - this.timestampMillis = timestampMillis; + public CpuCollectionData(final double cpuUsagePercentage, final @NotNull SentryDate timestamp) { this.cpuUsagePercentage = cpuUsagePercentage; this.timestamp = timestamp; } @@ -22,10 +17,6 @@ public CpuCollectionData( return timestamp; } - public long getTimestampMillis() { - return timestampMillis; - } - public double getCpuUsagePercentage() { return cpuUsagePercentage; } diff --git a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java index 603c452364..9ede59ba07 100644 --- a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java +++ b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java @@ -13,10 +13,9 @@ public void setup() {} @Override public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { - final long now = System.currentTimeMillis(); final long usedMemory = runtime.totalMemory() - runtime.freeMemory(); MemoryCollectionData memoryData = - new MemoryCollectionData(now, usedMemory, new SentryNanotimeDate()); + new MemoryCollectionData(usedMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } diff --git a/sentry/src/main/java/io/sentry/MemoryCollectionData.java b/sentry/src/main/java/io/sentry/MemoryCollectionData.java index 6a85fa28b0..1155e00b4b 100644 --- a/sentry/src/main/java/io/sentry/MemoryCollectionData.java +++ b/sentry/src/main/java/io/sentry/MemoryCollectionData.java @@ -5,35 +5,25 @@ @ApiStatus.Internal public final class MemoryCollectionData { - final long timestampMillis; final long usedHeapMemory; final long usedNativeMemory; final @NotNull SentryDate timestamp; public MemoryCollectionData( - final long timestampMillis, - final long usedHeapMemory, - final long usedNativeMemory, - final @NotNull SentryDate timestamp) { - this.timestampMillis = timestampMillis; + final long usedHeapMemory, final long usedNativeMemory, final @NotNull SentryDate timestamp) { this.usedHeapMemory = usedHeapMemory; this.usedNativeMemory = usedNativeMemory; this.timestamp = timestamp; } - public MemoryCollectionData( - final long timestampMillis, final long usedHeapMemory, final @NotNull SentryDate timestamp) { - this(timestampMillis, usedHeapMemory, -1, timestamp); + public MemoryCollectionData(final long usedHeapMemory, final @NotNull SentryDate timestamp) { + this(usedHeapMemory, -1, timestamp); } public @NotNull SentryDate getTimestamp() { return timestamp; } - public long getTimestampMillis() { - return timestampMillis; - } - public long getUsedHeapMemory() { return usedHeapMemory; } diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index 5a41c8facd..7e076bba78 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -23,7 +23,6 @@ class JavaMemoryCollectorTest { assertNotNull(memoryData) assertEquals(-1, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) - assertNotEquals(0, memoryData.timestampMillis) assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt index ad821b952c..76866b09e6 100644 --- a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt +++ b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt @@ -19,8 +19,8 @@ class PerformanceCollectionDataTest { val data = fixture.getSut() val t1 = mock() val t2 = mock() - val memData1 = MemoryCollectionData(0, 0, 0, t1) - val memData2 = MemoryCollectionData(1, 1, 1, t2) + val memData1 = MemoryCollectionData(0, 0, t1) + val memData2 = MemoryCollectionData(1, 1, t2) data.addMemoryData(memData1) data.addMemoryData(memData2) val savedMemoryData = data.memoryData @@ -33,8 +33,8 @@ class PerformanceCollectionDataTest { val data = fixture.getSut() val t1 = mock() val t2 = mock() - val cpuData1 = CpuCollectionData(0, 0.0, t1) - val cpuData2 = CpuCollectionData(1, 1.0, t2) + val cpuData1 = CpuCollectionData(0.0, t1) + val cpuData2 = CpuCollectionData(1.0, t2) data.addCpuData(cpuData1) data.addCpuData(cpuData2) val savedCpuData = data.cpuData @@ -45,7 +45,7 @@ class PerformanceCollectionDataTest { @Test fun `null values are ignored`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0, mock()) + val cpuData1 = CpuCollectionData(0.0, mock()) data.addCpuData(cpuData1) data.addCpuData(null) data.addMemoryData(null) From cc5a048e704a098a8b7ff027c6c27cbadb190ee0 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 11 Nov 2024 17:38:35 +0100 Subject: [PATCH 3/5] merged v8 branch --- .../core/AndroidContinuousProfiler.java | 12 +-- .../sentry/android/core/AndroidProfiler.java | 80 +++++++++---------- sentry/api/sentry.api | 2 - 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 3541ef14d6..99c9618498 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -17,8 +17,6 @@ import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.RejectedExecutionException; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -91,8 +89,7 @@ private void init() { (int) SECONDS.toMicros(1) / profilingTracesHz, frameMetricsCollector, null, - logger, - buildInfoProvider); + logger); } public synchronized void setScopes(final @NotNull IScopes scopes) { @@ -127,14 +124,13 @@ public synchronized void start() { chunkId = new SentryId(); } - try { closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } catch (RejectedExecutionException e) { logger.log( - SentryLevel.ERROR, - "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", - e); + SentryLevel.ERROR, + "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", + e); } } 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 7d8f47ad0e..e51e06fc6e 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 @@ -13,8 +13,8 @@ import io.sentry.PerformanceCollectionData; import io.sentry.SentryDate; import io.sentry.SentryLevel; -import io.sentry.SentryUUID; import io.sentry.SentryNanotimeDate; +import io.sentry.SentryUUID; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; import io.sentry.profilemeasurements.ProfileMeasurementValue; @@ -144,47 +144,47 @@ public AndroidProfiler( new SentryFrameMetricsCollector.FrameMetricsCollectorListener() { float lastRefreshRate = 0; - @Override - public void onFrameMetricCollected( - final long frameStartNanos, - final long frameEndNanos, - final long durationNanos, - final long delayNanos, - final boolean isSlow, - final boolean isFrozen, - final float refreshRate) { - // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), - // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp - // relative to profileStartNanos - final SentryDate timestamp = new SentryNanotimeDate(); - final long frameTimestampRelativeNanos = - frameEndNanos - - System.nanoTime() - + SystemClock.elapsedRealtimeNanos() - - profileStartNanos; + @Override + public void onFrameMetricCollected( + final long frameStartNanos, + final long frameEndNanos, + final long durationNanos, + final long delayNanos, + final boolean isSlow, + final boolean isFrozen, + final float refreshRate) { + // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), + // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp + // relative to profileStartNanos + final SentryDate timestamp = new SentryNanotimeDate(); + final long frameTimestampRelativeNanos = + frameEndNanos + - System.nanoTime() + + SystemClock.elapsedRealtimeNanos() + - profileStartNanos; - // We don't allow negative relative timestamps. - // So we add a check, even if this should never happen. - if (frameTimestampRelativeNanos < 0) { - return; - } - if (isFrozen) { - frozenFrameRenderMeasurements.addLast( - new ProfileMeasurementValue( - frameTimestampRelativeNanos, durationNanos, timestamp)); - } else if (isSlow) { - slowFrameRenderMeasurements.addLast( - new ProfileMeasurementValue( - frameTimestampRelativeNanos, durationNanos, timestamp)); - } - if (refreshRate != lastRefreshRate) { - lastRefreshRate = refreshRate; - screenFrameRateMeasurements.addLast( - new ProfileMeasurementValue( - frameTimestampRelativeNanos, refreshRate, timestamp)); + // We don't allow negative relative timestamps. + // So we add a check, even if this should never happen. + if (frameTimestampRelativeNanos < 0) { + return; + } + if (isFrozen) { + frozenFrameRenderMeasurements.addLast( + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); + } else if (isSlow) { + slowFrameRenderMeasurements.addLast( + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); + } + if (refreshRate != lastRefreshRate) { + lastRefreshRate = refreshRate; + screenFrameRateMeasurements.addLast( + new ProfileMeasurementValue( + frameTimestampRelativeNanos, refreshRate, timestamp)); + } } - } - }); + }); // We stop profiling after a timeout to avoid huge profiles to be sent try { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 213575519f..babc5d1db4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2565,7 +2565,6 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/protocol/SentryId; public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -2646,7 +2645,6 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromCheckIn (Lio/sentry/ISerializer;Lio/sentry/CheckIn;)Lio/sentry/SentryEnvelopeItem; public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; - public static fun fromMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem; From 3c9a35a113a4e8df95a4f863e6ebdd6b6990ff80 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 12 Nov 2024 11:15:38 +0100 Subject: [PATCH 4/5] continuous profiler doesn't stop anymore when an error occurs, but continue scheduling restart --- .../core/AndroidContinuousProfiler.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 99c9618498..b8c016e4ac 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -159,19 +159,18 @@ private synchronized void stop(final boolean restartProfiler) { // check if profiler end successfully if (endData == null) { - // A problem occurred. Profile chunk is not captured. Let's reset ids. - chunkId = SentryId.EMPTY_ID; - profilerId = SentryId.EMPTY_ID; - return; - } - - // The scopes can be null if the profiler is started before the SDK is initialized (app start - // profiling), meaning there's no scopes to send the chunks. In that case, we store the data - // in a list and send it when the next chunk is finished. - synchronized (payloadBuilders) { - payloadBuilders.add( - new ProfileChunk.Builder( - profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + logger.log( + SentryLevel.ERROR, + "An error occurred while collecting a profile chunk, and it won't be sent."); + } else { + // The scopes can be null if the profiler is started before the SDK is initialized (app start + // profiling), meaning there's no scopes to send the chunks. In that case, we store the data + // in a list and send it when the next chunk is finished. + synchronized (payloadBuilders) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + } } isRunning = false; From 5ebb53629aa16d33cf542a37a5ab8fa2f232ae37 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 14 Nov 2024 15:56:20 +0100 Subject: [PATCH 5/5] Instantiate continuous profiling v8 (p3) (#3725) * added profile context to SentryTracer * removed isProfilingEnabled from AndroidContinuousProfiler, as it's useless * added continuous profiler to SentryOptions * added DefaultTransactionPerformanceCollector to AndroidContinuousProfiler * updated DefaultTransactionPerformanceCollector to work with string ids other than transactions * fixed ProfileChunk measurements being modifiable from other code * added thread id and name to SpanContext.data * added profiler_id to span data * close continuous profiler on scopes close * renamed TransactionPerformanceCollector to CompositePerformanceCollector * added SpanContext.data ser/deser Handle App Start Continuous Profiling v8 (p4) (#3730) * create app start continuous profiler instead of transaction profiler, based on config * updated SentryAppStartProfilingOptions with isContinuousProfilingEnabled flag * updated SentryOptions with isContinuousProfilingEnabled() method * cut profiler setup out in a specific function to improve readability of AndroidOptionsInitializer Add new APIs for Continuous Profiling v8 (p5) (#3844) * AndroidContinuousProfiler now retrieve the scopes on start() * removed profilesSampleRate from sample app to enable continuous profiling * added Sentry.startProfiler and Sentry.stopProfiler APIs --- .../api/sentry-android-core.api | 6 +- .../core/AndroidContinuousProfiler.java | 53 +++++--- .../core/AndroidOptionsInitializer.java | 87 ++++++++++--- .../core/SentryPerformanceProvider.java | 93 +++++++++----- .../internal/util/AndroidThreadChecker.java | 5 + .../core/performance/AppStartMetrics.java | 21 +++- .../core/AndroidContinuousProfilerTest.kt | 86 +++++++++---- .../core/AndroidOptionsInitializerTest.kt | 116 +++++++++++++++++- .../core/SentryPerformanceProviderTest.kt | 65 +++++++++- .../internal/util/AndroidThreadCheckerTest.kt | 20 +++ .../core/performance/AppStartMetricsTest.kt | 32 ++++- .../android/sqlite/SQLiteSpanManagerTest.kt | 2 + .../api/sentry-opentelemetry-extra.api | 2 +- .../sentry/opentelemetry/OtelSpanFactory.java | 4 +- .../src/main/AndroidManifest.xml | 2 +- .../sentry/samples/android/MyApplication.java | 2 + .../webflux/SentryWebFluxTracingFilterTest.kt | 2 +- .../webflux/SentryWebFluxTracingFilterTest.kt | 2 +- sentry/api/sentry.api | 100 ++++++++++----- ...ava => CompositePerformanceCollector.java} | 11 +- ...DefaultCompositePerformanceCollector.java} | 36 ++++-- .../java/io/sentry/DefaultSpanFactory.java | 4 +- .../src/main/java/io/sentry/HubAdapter.java | 10 ++ .../main/java/io/sentry/HubScopesWrapper.java | 10 ++ .../java/io/sentry/IContinuousProfiler.java | 6 +- sentry/src/main/java/io/sentry/IScopes.java | 4 + .../src/main/java/io/sentry/ISpanFactory.java | 2 +- ...=> NoOpCompositePerformanceCollector.java} | 18 ++- .../io/sentry/NoOpContinuousProfiler.java | 9 +- sentry/src/main/java/io/sentry/NoOpHub.java | 6 + .../src/main/java/io/sentry/NoOpScopes.java | 6 + .../main/java/io/sentry/NoOpSpanFactory.java | 2 +- .../src/main/java/io/sentry/ProfileChunk.java | 2 +- .../main/java/io/sentry/ProfileContext.java | 4 +- sentry/src/main/java/io/sentry/Scopes.java | 39 +++++- .../main/java/io/sentry/ScopesAdapter.java | 10 ++ sentry/src/main/java/io/sentry/Sentry.java | 10 ++ .../SentryAppStartProfilingOptions.java | 21 ++++ .../main/java/io/sentry/SentryOptions.java | 58 +++++++-- .../src/main/java/io/sentry/SentryTracer.java | 46 ++++--- .../src/main/java/io/sentry/SpanContext.java | 11 ++ .../java/io/sentry/SpanDataConvention.java | 1 + .../io/sentry/protocol/SentryTransaction.java | 2 +- .../io/sentry/util/thread/IThreadChecker.java | 8 ++ .../sentry/util/thread/NoOpThreadChecker.java | 5 + .../io/sentry/util/thread/ThreadChecker.java | 5 + .../io/sentry/CheckInSerializationTest.kt | 5 +- ...faultCompositePerformanceCollectorTest.kt} | 69 ++++++++++- .../src/test/java/io/sentry/HubAdapterTest.kt | 10 ++ .../test/java/io/sentry/JsonSerializerTest.kt | 11 +- .../io/sentry/NoOpContinuousProfilerTest.kt | 7 ++ sentry/src/test/java/io/sentry/NoOpHubTest.kt | 6 + .../test/java/io/sentry/OutboxSenderTest.kt | 1 + .../test/java/io/sentry/ScopesAdapterTest.kt | 10 ++ sentry/src/test/java/io/sentry/ScopesTest.kt | 57 ++++++++- .../test/java/io/sentry/SentryOptionsTest.kt | 36 ++++-- sentry/src/test/java/io/sentry/SentryTest.kt | 47 +++++++ .../test/java/io/sentry/SentryTracerTest.kt | 93 ++++++++++++-- .../test/java/io/sentry/SpanContextTest.kt | 7 ++ .../protocol/SpanContextSerializationTest.kt | 3 + .../sentry/util/thread/ThreadCheckerTest.kt | 8 ++ .../test/resources/json/checkin_crontab.json | 7 +- .../test/resources/json/checkin_interval.json | 7 +- sentry/src/test/resources/json/contexts.json | 4 +- .../resources/json/sentry_base_event.json | 4 +- .../sentry_base_event_with_null_extra.json | 4 +- .../src/test/resources/json/sentry_event.json | 4 +- .../resources/json/sentry_replay_event.json | 4 +- .../resources/json/sentry_transaction.json | 4 +- ...sentry_transaction_legacy_date_format.json | 4 +- ...entry_transaction_no_measurement_unit.json | 4 +- .../src/test/resources/json/span_context.json | 4 +- 72 files changed, 1217 insertions(+), 249 deletions(-) rename sentry/src/main/java/io/sentry/{TransactionPerformanceCollector.java => CompositePerformanceCollector.java} (61%) rename sentry/src/main/java/io/sentry/{DefaultTransactionPerformanceCollector.java => DefaultCompositePerformanceCollector.java} (87%) rename sentry/src/main/java/io/sentry/{NoOpTransactionPerformanceCollector.java => NoOpCompositePerformanceCollector.java} (51%) rename sentry/src/test/java/io/sentry/{DefaultTransactionPerformanceCollectorTest.kt => DefaultCompositePerformanceCollectorTest.kt} (83%) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 20ebc767ee..dd4cfdf353 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -37,10 +37,10 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android } public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler { - public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ZILio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V public fun close ()V + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z - public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()V } @@ -447,6 +447,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V public fun clear ()V public fun getActivityLifecycleTimeSpans ()Ljava/util/List; + public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler; public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; @@ -465,6 +466,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V public fun registerApplicationForegroundCheck (Landroid/app/Application;)V public fun setAppLaunchedInForeground (Z)V + public fun setAppStartContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index b8c016e4ac..0536dcabc7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -4,11 +4,15 @@ import android.annotation.SuppressLint; import android.os.Build; +import io.sentry.CompositePerformanceCollector; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.IScopes; import io.sentry.ISentryExecutorService; +import io.sentry.NoOpScopes; +import io.sentry.PerformanceCollectionData; import io.sentry.ProfileChunk; +import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; @@ -28,7 +32,6 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private final @NotNull ILogger logger; private final @Nullable String profilingTracesDirPath; - private final boolean isProfilingEnabled; private final int profilingTracesHz; private final @NotNull ISentryExecutorService executorService; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -37,7 +40,8 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private @Nullable AndroidProfiler profiler = null; private boolean isRunning = false; private @Nullable IScopes scopes; - private @Nullable Future closeFuture; + private @Nullable Future stopFuture; + private @Nullable CompositePerformanceCollector performanceCollector; private final @NotNull List payloadBuilders = new ArrayList<>(); private @NotNull SentryId profilerId = SentryId.EMPTY_ID; private @NotNull SentryId chunkId = SentryId.EMPTY_ID; @@ -47,14 +51,12 @@ public AndroidContinuousProfiler( final @NotNull SentryFrameMetricsCollector frameMetricsCollector, final @NotNull ILogger logger, final @Nullable String profilingTracesDirPath, - final boolean isProfilingEnabled, final int profilingTracesHz, final @NotNull ISentryExecutorService executorService) { this.logger = logger; this.frameMetricsCollector = frameMetricsCollector; this.buildInfoProvider = buildInfoProvider; this.profilingTracesDirPath = profilingTracesDirPath; - this.isProfilingEnabled = isProfilingEnabled; this.profilingTracesHz = profilingTracesHz; this.executorService = executorService; } @@ -65,10 +67,6 @@ private void init() { return; } isInitialized = true; - if (!isProfilingEnabled) { - logger.log(SentryLevel.INFO, "Profiling is disabled in options."); - return; - } if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, @@ -92,11 +90,14 @@ private void init() { logger); } - public synchronized void setScopes(final @NotNull IScopes scopes) { - this.scopes = scopes; - } - public synchronized void start() { + if ((scopes == null || scopes != NoOpScopes.getInstance()) + && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { + this.scopes = Sentry.getCurrentScopes(); + this.performanceCollector = + Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector(); + } + // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; @@ -124,8 +125,12 @@ public synchronized void start() { chunkId = new SentryId(); } + if (performanceCollector != null) { + performanceCollector.start(chunkId.toString()); + } + try { - closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } catch (RejectedExecutionException e) { logger.log( SentryLevel.ERROR, @@ -140,8 +145,8 @@ public synchronized void stop() { @SuppressLint("NewApi") private synchronized void stop(final boolean restartProfiler) { - if (closeFuture != null) { - closeFuture.cancel(true); + if (stopFuture != null) { + stopFuture.cancel(true); } // check if profiler was created and it's running if (profiler == null || !isRunning) { @@ -154,8 +159,13 @@ private synchronized void stop(final boolean restartProfiler) { return; } - // todo add PerformanceCollectionData - final AndroidProfiler.ProfileEndData endData = profiler.endAndCollect(false, null); + List performanceCollectionData = null; + if (performanceCollector != null) { + performanceCollectionData = performanceCollector.stop(chunkId.toString()); + } + + final AndroidProfiler.ProfileEndData endData = + profiler.endAndCollect(false, performanceCollectionData); // check if profiler end successfully if (endData == null) { @@ -195,6 +205,11 @@ public synchronized void close() { stop(); } + @Override + public @NotNull SentryId getProfilerId() { + return profilerId; + } + private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { try { options @@ -224,7 +239,7 @@ public boolean isRunning() { @VisibleForTesting @Nullable - Future getCloseFuture() { - return closeFuture; + Future getStopFuture() { + return stopFuture; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 62900c25b8..605294f666 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -6,11 +6,14 @@ import android.content.Context; import android.content.pm.PackageInfo; import io.sentry.DeduplicateMultithreadedEventProcessor; -import io.sentry.DefaultTransactionPerformanceCollector; +import io.sentry.DefaultCompositePerformanceCollector; +import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.NoOpConnectionStatusProvider; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.NoOpTransactionProfiler; import io.sentry.ScopeType; import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; @@ -159,23 +162,23 @@ static void initializeIntegrationsAndProcessors( // Check if the profiler was already instantiated in the app start. // We use the Android profiler, that uses a global start/stop api, so we need to preserve the // state of the profiler, and it's only possible retaining the instance. + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + final @Nullable ITransactionProfiler appStartTransactionProfiler; + final @Nullable IContinuousProfiler appStartContinuousProfiler; try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) { - final @Nullable ITransactionProfiler appStartProfiler = - AppStartMetrics.getInstance().getAppStartProfiler(); - if (appStartProfiler != null) { - options.setTransactionProfiler(appStartProfiler); - AppStartMetrics.getInstance().setAppStartProfiler(null); - } else { - options.setTransactionProfiler( - new AndroidTransactionProfiler( - context, - options, - buildInfoProvider, - Objects.requireNonNull( - options.getFrameMetricsCollector(), - "options.getFrameMetricsCollector is required"))); - } + appStartTransactionProfiler = appStartMetrics.getAppStartProfiler(); + appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler(); + appStartMetrics.setAppStartProfiler(null); + appStartMetrics.setAppStartContinuousProfiler(null); } + + setupProfiler( + options, + context, + buildInfoProvider, + appStartTransactionProfiler, + appStartContinuousProfiler); + options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger())); options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger())); @@ -223,7 +226,7 @@ static void initializeIntegrationsAndProcessors( "options.getFrameMetricsCollector is required"))); } } - options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options)); + options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options)); if (options.getCacheDirPath() != null) { if (options.isEnableScopePersistence()) { @@ -233,6 +236,56 @@ static void initializeIntegrationsAndProcessors( } } + /** Setup the correct profiler (transaction or continuous) based on the options. */ + private static void setupProfiler( + final @NotNull SentryAndroidOptions options, + final @NotNull Context context, + final @NotNull BuildInfoProvider buildInfoProvider, + final @Nullable ITransactionProfiler appStartTransactionProfiler, + final @Nullable IContinuousProfiler appStartContinuousProfiler) { + if (options.isProfilingEnabled() || options.getProfilesSampleRate() != null) { + options.setContinuousProfiler(NoOpContinuousProfiler.getInstance()); + // This is a safeguard, but it should never happen, as the app start profiler should be the + // continuous one. + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } + if (appStartTransactionProfiler != null) { + options.setTransactionProfiler(appStartTransactionProfiler); + } else { + options.setTransactionProfiler( + new AndroidTransactionProfiler( + context, + options, + buildInfoProvider, + Objects.requireNonNull( + options.getFrameMetricsCollector(), + "options.getFrameMetricsCollector is required"))); + } + } else { + options.setTransactionProfiler(NoOpTransactionProfiler.getInstance()); + // This is a safeguard, but it should never happen, as the app start profiler should be the + // transaction one. + if (appStartTransactionProfiler != null) { + appStartTransactionProfiler.close(); + } + if (appStartContinuousProfiler != null) { + options.setContinuousProfiler(appStartContinuousProfiler); + } else { + options.setContinuousProfiler( + new AndroidContinuousProfiler( + buildInfoProvider, + Objects.requireNonNull( + options.getFrameMetricsCollector(), + "options.getFrameMetricsCollector is required"), + options.getLogger(), + options.getProfilingTracesDirPath(), + options.getProfilingTracesHz(), + options.getExecutorService())); + } + } + } + static void installDefaultIntegrations( final @NotNull Context context, final @NotNull SentryAndroidOptions options, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 64a1ceda60..1d76775b3f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -12,6 +12,7 @@ import android.os.Process; import android.os.SystemClock; import androidx.annotation.NonNull; +import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; @@ -100,6 +101,11 @@ public void shutdown() { if (appStartProfiler != null) { appStartProfiler.close(); } + final @Nullable IContinuousProfiler appStartContinuousProfiler = + AppStartMetrics.getInstance().getAppStartContinuousProfiler(); + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } } } @@ -132,40 +138,18 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } + if (profilingOptions.isContinuousProfilingEnabled()) { + createAndStartContinuousProfiler(context, profilingOptions, appStartMetrics); + return; + } + if (!profilingOptions.isProfilingEnabled()) { logger.log( SentryLevel.INFO, "Profiling is not enabled. App start profiling will not start."); return; } - final @NotNull TracesSamplingDecision appStartSamplingDecision = - new TracesSamplingDecision( - profilingOptions.isTraceSampled(), - profilingOptions.getTraceSampleRate(), - profilingOptions.isProfileSampled(), - profilingOptions.getProfileSampleRate()); - // We store any sampling decision, so we can respect it when the first transaction starts - appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision); - - if (!(appStartSamplingDecision.getProfileSampled() - && appStartSamplingDecision.getSampled())) { - logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); - return; - } - logger.log(SentryLevel.DEBUG, "App start profiling started."); - - final @NotNull ITransactionProfiler appStartProfiler = - new AndroidTransactionProfiler( - context, - buildInfoProvider, - new SentryFrameMetricsCollector(context, logger, buildInfoProvider), - logger, - profilingOptions.getProfilingTracesDirPath(), - profilingOptions.isProfilingEnabled(), - profilingOptions.getProfilingTracesHz(), - new SentryExecutorService()); - appStartMetrics.setAppStartProfiler(appStartProfiler); - appStartProfiler.start(); + createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics); } catch (FileNotFoundException e) { logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e); @@ -174,6 +158,59 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri } } + private void createAndStartContinuousProfiler( + final @NotNull Context context, + final @NotNull SentryAppStartProfilingOptions profilingOptions, + final @NotNull AppStartMetrics appStartMetrics) { + final @NotNull IContinuousProfiler appStartContinuousProfiler = + new AndroidContinuousProfiler( + buildInfoProvider, + new SentryFrameMetricsCollector( + context.getApplicationContext(), logger, buildInfoProvider), + logger, + profilingOptions.getProfilingTracesDirPath(), + profilingOptions.getProfilingTracesHz(), + new SentryExecutorService()); + appStartMetrics.setAppStartProfiler(null); + appStartMetrics.setAppStartContinuousProfiler(appStartContinuousProfiler); + logger.log(SentryLevel.DEBUG, "App start continuous profiling started."); + appStartContinuousProfiler.start(); + } + + private void createAndStartTransactionProfiler( + final @NotNull Context context, + final @NotNull SentryAppStartProfilingOptions profilingOptions, + final @NotNull AppStartMetrics appStartMetrics) { + final @NotNull TracesSamplingDecision appStartSamplingDecision = + new TracesSamplingDecision( + profilingOptions.isTraceSampled(), + profilingOptions.getTraceSampleRate(), + profilingOptions.isProfileSampled(), + profilingOptions.getProfileSampleRate()); + // We store any sampling decision, so we can respect it when the first transaction starts + appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision); + + if (!(appStartSamplingDecision.getProfileSampled() && appStartSamplingDecision.getSampled())) { + logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); + return; + } + + final @NotNull ITransactionProfiler appStartProfiler = + new AndroidTransactionProfiler( + context, + buildInfoProvider, + new SentryFrameMetricsCollector(context, logger, buildInfoProvider), + logger, + profilingOptions.getProfilingTracesDirPath(), + profilingOptions.isProfilingEnabled(), + profilingOptions.getProfilingTracesHz(), + new SentryExecutorService()); + appStartMetrics.setAppStartContinuousProfiler(null); + appStartMetrics.setAppStartProfiler(appStartProfiler); + logger.log(SentryLevel.DEBUG, "App start profiling started."); + appStartProfiler.start(); + } + @SuppressLint("NewApi") private void onAppLaunched( final @Nullable Context context, final @NotNull AppStartMetrics appStartMetrics) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java index 15781d711f..ccd4a92b27 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java @@ -39,6 +39,11 @@ public boolean isMainThread() { return isMainThread(Thread.currentThread()); } + @Override + public @NotNull String getCurrentThreadName() { + return isMainThread() ? "main" : Thread.currentThread().getName(); + } + @Override public boolean isMainThread(final @NotNull SentryThread sentryThread) { final Long threadId = sentryThread.getId(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 996c6ab171..f43b44a5ad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.sentry.IContinuousProfiler; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.SentryDate; @@ -57,6 +58,7 @@ public enum AppStartType { private final @NotNull Map contentProviderOnCreates; private final @NotNull List activityLifecycles; private @Nullable ITransactionProfiler appStartProfiler = null; + private @Nullable IContinuousProfiler appStartContinuousProfiler = null; private @Nullable TracesSamplingDecision appStartSamplingDecision = null; private @Nullable SentryDate onCreateTime = null; private boolean appLaunchTooLong = false; @@ -186,6 +188,10 @@ public void clear() { appStartProfiler.close(); } appStartProfiler = null; + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } + appStartContinuousProfiler = null; appStartSamplingDecision = null; appLaunchTooLong = false; appLaunchedInForeground = false; @@ -201,6 +207,15 @@ public void setAppStartProfiler(final @Nullable ITransactionProfiler appStartPro this.appStartProfiler = appStartProfiler; } + public @Nullable IContinuousProfiler getAppStartContinuousProfiler() { + return appStartContinuousProfiler; + } + + public void setAppStartContinuousProfiler( + final @Nullable IContinuousProfiler appStartContinuousProfiler) { + this.appStartContinuousProfiler = appStartContinuousProfiler; + } + public void setAppStartSamplingDecision( final @Nullable TracesSamplingDecision appStartSamplingDecision) { this.appStartSamplingDecision = appStartSamplingDecision; @@ -259,11 +274,15 @@ private void checkCreateTimeOnMain(final @NotNull Application application) { if (onCreateTime == null) { appLaunchedInForeground = false; - // we stop the app start profiler, as it's useless and likely to timeout + // we stop the app start profilers, as they are useless and likely to timeout if (appStartProfiler != null && appStartProfiler.isRunning()) { appStartProfiler.close(); appStartProfiler = null; } + if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) { + appStartContinuousProfiler.close(); + appStartContinuousProfiler = null; + } } application.unregisterActivityLifecycleCallbacks(instance); }); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index 5878354e70..70ed75186c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -4,17 +4,25 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.CompositePerformanceCollector +import io.sentry.CpuCollectionData import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ISentryExecutorService +import io.sentry.MemoryCollectionData +import io.sentry.PerformanceCollectionData +import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.SentryNanotimeDate import io.sentry.SentryTracer import io.sentry.TransactionContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector +import io.sentry.profilemeasurements.ProfileMeasurement import io.sentry.test.DeferredExecutorService import io.sentry.test.getProperty import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.check import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -28,6 +36,7 @@ import java.util.concurrent.Future import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -70,10 +79,9 @@ class AndroidContinuousProfilerTest { frameMetricsCollector, options.logger, options.profilingTracesDirPath, - options.isProfilingEnabled, options.profilingTracesHz, options.executorService - ).also { it.setScopes(scopes) } + ) } } @@ -111,6 +119,8 @@ class AndroidContinuousProfilerTest { // Profiler doesn't start if the folder doesn't exists. // Usually it's generated when calling Sentry.init, but for tests we can create it manually. File(fixture.options.profilingTracesDirPath!!).mkdirs() + + Sentry.setCurrentScopes(fixture.scopes) } @AfterTest @@ -149,26 +159,12 @@ class AndroidContinuousProfilerTest { } @Test - fun `profiler on profilesSampleRate=0 false`() { + fun `profiler ignores profilesSampleRate`() { val profiler = fixture.getSut { it.profilesSampleRate = 0.0 } profiler.start() - assertFalse(profiler.isRunning) - } - - @Test - fun `profiler evaluates if profiling is enabled in options only on first start`() { - // We create the profiler, and nothing goes wrong - val profiler = fixture.getSut { - it.profilesSampleRate = 0.0 - } - verify(fixture.mockLogger, never()).log(SentryLevel.INFO, "Profiling is disabled in options.") - - // Regardless of how many times the profiler is started, the option is evaluated and logged only once - profiler.start() - profiler.start() - verify(fixture.mockLogger, times(1)).log(SentryLevel.INFO, "Profiling is disabled in options.") + assertTrue(profiler.isRunning) } @Test @@ -269,6 +265,27 @@ class AndroidContinuousProfilerTest { verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Error while stopping profiling: "), any()) } + @Test + fun `profiler starts performance collector on start`() { + val performanceCollector = mock() + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut() + verify(performanceCollector, never()).start(any()) + profiler.start() + verify(performanceCollector).start(any()) + } + + @Test + fun `profiler stops performance collector on stop`() { + val performanceCollector = mock() + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut() + profiler.start() + verify(performanceCollector, never()).stop(any()) + profiler.stop() + verify(performanceCollector).stop(any()) + } + @Test fun `profiler stops collecting frame metrics when it stops`() { val profiler = fixture.getSut() @@ -294,9 +311,9 @@ class AndroidContinuousProfilerTest { val scheduledJob = androidProfiler?.getProperty?>("scheduledFinish") assertNull(scheduledJob) - val closeFuture = profiler.closeFuture - assertNotNull(closeFuture) - assertTrue(closeFuture.isCancelled) + val stopFuture = profiler.stopFuture + assertNotNull(stopFuture) + assertTrue(stopFuture.isCancelled) } @Test @@ -333,6 +350,33 @@ class AndroidContinuousProfilerTest { verify(fixture.scopes).captureProfileChunk(any()) } + @Test + fun `profiler sends chunk with measurements`() { + val executorService = DeferredExecutorService() + val performanceCollector = mock() + val collectionData = PerformanceCollectionData() + + collectionData.addMemoryData(MemoryCollectionData(2, 3, SentryNanotimeDate())) + collectionData.addCpuData(CpuCollectionData(3.0, SentryNanotimeDate())) + whenever(performanceCollector.stop(any())).thenReturn(listOf(collectionData)) + + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + profiler.stop() + // We run the executor service to send the profile chunk + executorService.runAll() + verify(fixture.scopes).captureProfileChunk( + check { + assertContains(it.measurements, ProfileMeasurement.ID_CPU_USAGE) + assertContains(it.measurements, ProfileMeasurement.ID_MEMORY_FOOTPRINT) + assertContains(it.measurements, ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT) + } + ) + } + @Test fun `profiler sends another chunk on stop`() { val executorService = DeferredExecutorService() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 95ca59a06e..56571b4431 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -6,14 +6,19 @@ import android.os.Build import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.DefaultTransactionPerformanceCollector +import io.sentry.DefaultCompositePerformanceCollector +import io.sentry.IContinuousProfiler import io.sentry.ILogger +import io.sentry.ITransactionProfiler import io.sentry.MainEventProcessor +import io.sentry.NoOpContinuousProfiler +import io.sentry.NoOpTransactionProfiler import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidThreadChecker +import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.replay.ReplayIntegration import io.sentry.android.timber.SentryTimberIntegration @@ -35,6 +40,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -344,11 +350,113 @@ class AndroidOptionsInitializerTest { } @Test - fun `init should set Android transaction profiler`() { + fun `init should set Android continuous profiler`() { fixture.initSut() + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.transactionProfiler, NoOpTransactionProfiler.getInstance()) + assertTrue(fixture.sentryOptions.continuousProfiler is AndroidContinuousProfiler) + } + + @Test + fun `init with profilesSampleRate should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampleRate = 1.0 + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init with profilesSampleRate 0 should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampleRate = 0.0 + }) + assertNotNull(fixture.sentryOptions.transactionProfiler) assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init with profilesSampler should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init reuses transaction profiler of appStartMetrics, if exists`() { + val appStartProfiler = mock() + AppStartMetrics.getInstance().appStartProfiler = appStartProfiler + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertEquals(appStartProfiler, fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init reuses continuous profiler of appStartMetrics, if exists`() { + val appStartContinuousProfiler = mock() + AppStartMetrics.getInstance().appStartContinuousProfiler = appStartContinuousProfiler + fixture.initSut() + + assertEquals(fixture.sentryOptions.transactionProfiler, NoOpTransactionProfiler.getInstance()) + assertEquals(appStartContinuousProfiler, fixture.sentryOptions.continuousProfiler) + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init with transaction profiling closes continuous profiler of appStartMetrics`() { + val appStartContinuousProfiler = mock() + AppStartMetrics.getInstance().appStartContinuousProfiler = appStartContinuousProfiler + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertNotEquals(NoOpTransactionProfiler.getInstance(), fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + + // app start profiler is closed, because it will never be used + verify(appStartContinuousProfiler).close() + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init with continuous profiling closes transaction profiler of appStartMetrics`() { + val appStartProfiler = mock() + AppStartMetrics.getInstance().appStartProfiler = appStartProfiler + fixture.initSut() + + assertEquals(NoOpTransactionProfiler.getInstance(), fixture.sentryOptions.transactionProfiler) + assertNotNull(fixture.sentryOptions.continuousProfiler) + assertNotEquals(NoOpContinuousProfiler.getInstance(), fixture.sentryOptions.continuousProfiler) + + // app start profiler is closed, because it will never be used + verify(appStartProfiler).close() + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) } @Test @@ -663,10 +771,10 @@ class AndroidOptionsInitializerTest { } @Test - fun `DefaultTransactionPerformanceCollector is set to options`() { + fun `DefaultCompositePerformanceCollector is set to options`() { fixture.initSut() - assertIs(fixture.sentryOptions.transactionPerformanceCollector) + assertIs(fixture.sentryOptions.compositePerformanceCollector) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 26a76af30e..237bc54867 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -176,6 +176,7 @@ class SentryPerformanceProviderTest { fun `when config file does not exists, nothing happens`() { fixture.getSut() assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger, never()).log(any(), any()) } @@ -186,6 +187,7 @@ class SentryPerformanceProviderTest { config.setReadable(false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger, never()).log(any(), any()) } @@ -195,6 +197,7 @@ class SentryPerformanceProviderTest { config.createNewFile() } assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger).log( eq(SentryLevel.WARNING), eq("Unable to deserialize the SentryAppStartProfilingOptions. App start profiling will not start.") @@ -204,7 +207,7 @@ class SentryPerformanceProviderTest { @Test fun `when profiling is disabled, profiler is not started`() { fixture.getSut { config -> - writeConfig(config, profilingEnabled = false) + writeConfig(config, profilingEnabled = false, continuousProfilingEnabled = false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) verify(fixture.logger).log( @@ -213,10 +216,22 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when continuous profiling is disabled, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, continuousProfilingEnabled = false, profilingEnabled = false) + } + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + verify(fixture.logger).log( + eq(SentryLevel.INFO), + eq("Profiling is not enabled. App start profiling will not start.") + ) + } + @Test fun `when trace is not sampled, profiler is not started and sample decision is stored`() { fixture.getSut { config -> - writeConfig(config, traceSampled = false, profileSampled = true) + writeConfig(config, continuousProfilingEnabled = false, traceSampled = false, profileSampled = true) } assertNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) @@ -232,7 +247,7 @@ class SentryPerformanceProviderTest { @Test fun `when profile is not sampled, profiler is not started and sample decision is stored`() { fixture.getSut { config -> - writeConfig(config, traceSampled = true, profileSampled = false) + writeConfig(config, continuousProfilingEnabled = false, traceSampled = true, profileSampled = false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) @@ -244,11 +259,26 @@ class SentryPerformanceProviderTest { ) } + // This case should never happen in reality, but it's technically possible to have such configuration @Test - fun `when profiler starts, it is set in AppStartMetrics`() { + fun `when both transaction and continuous profilers are enabled, only continuous profiler is created`() { fixture.getSut { config -> writeConfig(config) } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertTrue(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start continuous profiling started.") + ) + } + + @Test + fun `when profiler starts, it is set in AppStartMetrics`() { + fixture.getSut { config -> + writeConfig(config, continuousProfilingEnabled = false) + } assertNotNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) assertTrue(AppStartMetrics.getInstance().appStartProfiler!!.isRunning) @@ -260,19 +290,43 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when continuous profiler starts, it is set in AppStartMetrics`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = false) + } + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertTrue(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start continuous profiling started.") + ) + } + @Test fun `when provider is closed, profiler is stopped`() { val provider = fixture.getSut { config -> - writeConfig(config) + writeConfig(config, continuousProfilingEnabled = false) } provider.shutdown() assertNotNull(AppStartMetrics.getInstance().appStartProfiler) assertFalse(AppStartMetrics.getInstance().appStartProfiler!!.isRunning) } + @Test + fun `when provider is closed, continuous profiler is stopped`() { + val provider = fixture.getSut { config -> + writeConfig(config, profilingEnabled = false) + } + provider.shutdown() + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertFalse(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + } + private fun writeConfig( configFile: File, profilingEnabled: Boolean = true, + continuousProfilingEnabled: Boolean = true, traceSampled: Boolean = true, traceSampleRate: Double = 1.0, profileSampled: Boolean = true, @@ -281,6 +335,7 @@ class SentryPerformanceProviderTest { ) { val appStartProfilingOptions = SentryAppStartProfilingOptions() appStartProfilingOptions.isProfilingEnabled = profilingEnabled + appStartProfilingOptions.isContinuousProfilingEnabled = continuousProfilingEnabled appStartProfilingOptions.isTraceSampled = traceSampled appStartProfilingOptions.traceSampleRate = traceSampleRate appStartProfilingOptions.isProfileSampled = profileSampled diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt index eb59f0732e..0b3729f8ce 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.protocol.SentryThread import org.junit.runner.RunWith import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -44,4 +45,23 @@ class AndroidThreadCheckerTest { } assertFalse(AndroidThreadChecker.getInstance().isMainThread(sentryThread)) } + + @Test + fun `currentThreadName returns main when called on the main thread`() { + val thread = Thread.currentThread() + thread.name = "test" + assertEquals("main", AndroidThreadChecker.getInstance().currentThreadName) + } + + @Test + fun `currentThreadName returns the name of the current thread`() { + var threadName = "" + val thread = Thread { + threadName = AndroidThreadChecker.getInstance().currentThreadName + } + thread.name = "test" + thread.start() + thread.join() + assertEquals("test", threadName) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index eb0e85dc28..8d3cf062b2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -5,6 +5,7 @@ import android.content.ContentProvider import android.os.Build import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.IContinuousProfiler import io.sentry.ITransactionProfiler import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess @@ -56,7 +57,8 @@ class AppStartMetricsTest { metrics.addActivityLifecycleTimeSpans(ActivityLifecycleTimeSpan()) AppStartMetrics.onApplicationCreate(mock()) AppStartMetrics.onContentProviderCreate(mock()) - metrics.setAppStartProfiler(mock()) + metrics.appStartProfiler = mock() + metrics.appStartContinuousProfiler = mock() metrics.appStartSamplingDecision = mock() metrics.clear() @@ -69,6 +71,7 @@ class AppStartMetricsTest { assertTrue(metrics.activityLifecycleTimeSpans.isEmpty()) assertTrue(metrics.contentProviderOnCreateTimeSpans.isEmpty()) assertNull(metrics.appStartProfiler) + assertNull(metrics.appStartContinuousProfiler) assertNull(metrics.appStartSamplingDecision) } @@ -196,6 +199,19 @@ class AppStartMetricsTest { verify(profiler).close() } + @Test + fun `if activity is never started, stops app start continuous profiler if running`() { + val profiler = mock() + whenever(profiler.isRunning).thenReturn(true) + AppStartMetrics.getInstance().appStartContinuousProfiler = profiler + + AppStartMetrics.getInstance().registerApplicationForegroundCheck(mock()) + // Job on main thread checks if activity was launched + Shadows.shadowOf(Looper.getMainLooper()).idle() + + verify(profiler).close() + } + @Test fun `if activity is started, does not stop app start profiler if running`() { val profiler = mock() @@ -210,6 +226,20 @@ class AppStartMetricsTest { verify(profiler, never()).close() } + @Test + fun `if activity is started, does not stop app start continuous profiler if running`() { + val profiler = mock() + whenever(profiler.isRunning).thenReturn(true) + AppStartMetrics.getInstance().appStartContinuousProfiler = profiler + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + + AppStartMetrics.getInstance().registerApplicationForegroundCheck(mock()) + // Job on main thread checks if activity was launched + Shadows.shadowOf(Looper.getMainLooper()).idle() + + verify(profiler, never()).close() + } + @Test fun `if app start span is longer than 1 minute, appStartTimeSpanWithFallback returns an empty span`() { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt index 17c37d69bf..b292f0d038 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt @@ -100,6 +100,7 @@ class SQLiteSpanManagerTest { fixture.options.threadChecker = mock() whenever(fixture.options.threadChecker.isMainThread).thenReturn(false) + whenever(fixture.options.threadChecker.currentThreadName).thenReturn("test") sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() @@ -114,6 +115,7 @@ class SQLiteSpanManagerTest { fixture.options.threadChecker = mock() whenever(fixture.options.threadChecker.isMainThread).thenReturn(true) + whenever(fixture.options.threadChecker.currentThreadName).thenReturn("test") sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() diff --git a/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api b/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api index bb749a7df1..80e3c55d1c 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api +++ b/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api @@ -21,7 +21,7 @@ public final class io/sentry/opentelemetry/OtelSpanContext : io/sentry/SpanConte public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/ISpan { diff --git a/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java index 7869f6dc96..19de213e18 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java +++ b/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.sentry.Baggage; +import io.sentry.CompositePerformanceCollector; import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.ISpanFactory; @@ -21,7 +22,6 @@ import io.sentry.TracesSamplingDecision; import io.sentry.TransactionContext; import io.sentry.TransactionOptions; -import io.sentry.TransactionPerformanceCollector; import io.sentry.protocol.SentryId; import io.sentry.util.SpanUtils; import java.util.concurrent.TimeUnit; @@ -39,7 +39,7 @@ public final class OtelSpanFactory implements ISpanFactory { @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + @Nullable CompositePerformanceCollector compositePerformanceCollector) { final @Nullable OtelSpanWrapper span = createSpanInternal( scopes, transactionOptions, null, context.getSamplingDecision(), context); diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index ac85411017..fa070ac644 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -110,7 +110,7 @@ - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index a4a1c5397a..572c4cdba7 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,12 +2,14 @@ import android.app.Application; import android.os.StrictMode; +import io.sentry.Sentry; /** Apps. main Application. */ public class MyApplication extends Application { @Override public void onCreate() { + Sentry.startProfiler(); strictMode(); super.onCreate(); diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt index 36a5ccf1e6..06b7a6f8d4 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt @@ -248,7 +248,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes, times(2)).isEnabled - verify(fixture.scopes, times(2)).options + verify(fixture.scopes, times(3)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt index 67b2c021f8..7711541cef 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt @@ -249,7 +249,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes).isEnabled - verify(fixture.scopes, times(2)).options + verify(fixture.scopes, times(3)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index babc5d1db4..f3a84217b5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -311,6 +311,16 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V } +public abstract interface class io/sentry/CompositePerformanceCollector { + public abstract fun close ()V + public abstract fun onSpanFinished (Lio/sentry/ISpan;)V + public abstract fun onSpanStarted (Lio/sentry/ISpan;)V + public abstract fun start (Lio/sentry/ITransaction;)V + public abstract fun start (Ljava/lang/String;)V + public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public abstract fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/CpuCollectionData { public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D @@ -367,6 +377,17 @@ public final class io/sentry/DeduplicateMultithreadedEventProcessor : io/sentry/ public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public final class io/sentry/DefaultCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun (Lio/sentry/SentryOptions;)V + public fun close ()V + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public fun ()V public fun close ()V @@ -377,16 +398,7 @@ public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public final class io/sentry/DefaultSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; -} - -public final class io/sentry/DefaultTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { - public fun (Lio/sentry/SentryOptions;)V - public fun close ()V - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public final class io/sentry/DiagnosticLogger : io/sentry/ILogger { @@ -607,8 +619,10 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -672,8 +686,10 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -700,8 +716,8 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract interface class io/sentry/IContinuousProfiler { public abstract fun close ()V + public abstract fun getProfilerId ()Lio/sentry/protocol/SentryId; public abstract fun isRunning ()Z - public abstract fun setScopes (Lio/sentry/IScopes;)V public abstract fun start ()V public abstract fun stop ()V } @@ -908,11 +924,13 @@ public abstract interface class io/sentry/IScopes { public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setTransaction (Ljava/lang/String;)V public abstract fun setUser (Lio/sentry/protocol/User;)V + public abstract fun startProfiler ()V public abstract fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public abstract fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public abstract fun stopProfiler ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1018,7 +1036,7 @@ public abstract interface class io/sentry/ISpan { public abstract interface class io/sentry/ISpanFactory { public abstract fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { @@ -1358,6 +1376,17 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static fun values ()[Lio/sentry/MonitorScheduleUnit; } +public final class io/sentry/NoOpCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun close ()V + public static fun getInstance ()Lio/sentry/NoOpCompositePerformanceCollector; + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { public fun ()V public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z @@ -1369,8 +1398,8 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfiler { public fun close ()V public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z - public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()V } @@ -1440,8 +1469,10 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1600,8 +1631,10 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1661,7 +1694,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public final class io/sentry/NoOpSpanFactory : io/sentry/ISpanFactory { public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; public static fun getInstance ()Lio/sentry/NoOpSpanFactory; } @@ -1718,15 +1751,6 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun updateEndDate (Lio/sentry/SentryDate;)Z } -public final class io/sentry/NoOpTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { - public fun close ()V - public static fun getInstance ()Lio/sentry/NoOpTransactionPerformanceCollector; - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/NoOpTransactionProfiler : io/sentry/ITransactionProfiler { public fun bindTransaction (Lio/sentry/ITransaction;)V public fun close ()V @@ -1860,7 +1884,7 @@ public final class io/sentry/ProfileChunk$JsonKeys { public fun ()V } -public class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { +public final class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V public fun (Lio/sentry/ProfileContext;)V @@ -2262,8 +2286,10 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2327,8 +2353,10 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2431,12 +2459,14 @@ public final class io/sentry/Sentry { public static fun setTag (Ljava/lang/String;Ljava/lang/String;)V public static fun setTransaction (Ljava/lang/String;)V public static fun setUser (Lio/sentry/protocol/User;)V + public static fun startProfiler ()V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public static fun stopProfiler ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2452,10 +2482,12 @@ public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSeri public fun getProfilingTracesHz ()I public fun getTraceSampleRate ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; + public fun isContinuousProfilingEnabled ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setContinuousProfilingEnabled (Z)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V @@ -2473,6 +2505,7 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; @@ -2899,9 +2932,11 @@ public class io/sentry/SentryOptions { public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; + public fun getCompositePerformanceCollector ()Lio/sentry/CompositePerformanceCollector; public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; + public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; @@ -2968,7 +3003,6 @@ public class io/sentry/SentryOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracesSampler ()Lio/sentry/SentryOptions$TracesSamplerCallback; - public fun getTransactionPerformanceCollector ()Lio/sentry/TransactionPerformanceCollector; public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; @@ -2976,6 +3010,7 @@ public class io/sentry/SentryOptions { public fun isAttachServerName ()Z public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z + public fun isContinuousProfilingEnabled ()Z public fun isDebug ()Z public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z @@ -3012,8 +3047,10 @@ public class io/sentry/SentryOptions { public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V + public fun setCompositePerformanceCollector (Lio/sentry/CompositePerformanceCollector;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V + public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -3092,7 +3129,6 @@ public class io/sentry/SentryOptions { public fun setTraceSampling (Z)V public fun setTracesSampleRate (Ljava/lang/Double;)V public fun setTracesSampler (Lio/sentry/SentryOptions$TracesSamplerCallback;)V - public fun setTransactionPerformanceCollector (Lio/sentry/TransactionPerformanceCollector;)V public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V @@ -3579,6 +3615,7 @@ public abstract interface class io/sentry/SpanDataConvention { public static final field HTTP_RESPONSE_CONTENT_LENGTH_KEY Ljava/lang/String; public static final field HTTP_START_TIMESTAMP Ljava/lang/String; public static final field HTTP_STATUS_CODE_KEY Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; public static final field THREAD_ID Ljava/lang/String; public static final field THREAD_NAME Ljava/lang/String; } @@ -3762,14 +3799,6 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun setWaitForChildren (Z)V } -public abstract interface class io/sentry/TransactionPerformanceCollector { - public abstract fun close ()V - public abstract fun onSpanFinished (Lio/sentry/ISpan;)V - public abstract fun onSpanStarted (Lio/sentry/ISpan;)V - public abstract fun start (Lio/sentry/ITransaction;)V - public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/TypeCheckHint { public static final field ANDROID_ACTIVITY Ljava/lang/String; public static final field ANDROID_CONFIGURATION Ljava/lang/String; @@ -6341,6 +6370,7 @@ public final class io/sentry/util/UrlUtils$UrlDetails { public abstract interface class io/sentry/util/thread/IThreadChecker { public abstract fun currentThreadSystemId ()J + public abstract fun getCurrentThreadName ()Ljava/lang/String; public abstract fun isMainThread ()Z public abstract fun isMainThread (J)Z public abstract fun isMainThread (Lio/sentry/protocol/SentryThread;)Z @@ -6350,6 +6380,7 @@ public abstract interface class io/sentry/util/thread/IThreadChecker { public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thread/IThreadChecker { public fun ()V public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/NoOpThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z @@ -6359,6 +6390,7 @@ public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thre public final class io/sentry/util/thread/ThreadChecker : io/sentry/util/thread/IThreadChecker { public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/ThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z diff --git a/sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/CompositePerformanceCollector.java similarity index 61% rename from sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/CompositePerformanceCollector.java index 7880d61197..e6238679a7 100644 --- a/sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/CompositePerformanceCollector.java @@ -5,10 +5,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public interface TransactionPerformanceCollector { +public interface CompositePerformanceCollector { + /** Starts collecting performance data and span related data (e.g. slow/frozen frames). */ void start(@NotNull ITransaction transaction); + /** Starts collecting performance data without span related data (e.g. slow/frozen frames). */ + void start(@NotNull String id); + /** * Called whenever a new span (including the top level transaction) is started. * @@ -23,9 +27,14 @@ public interface TransactionPerformanceCollector { */ void onSpanFinished(@NotNull ISpan span); + /** Stops collecting performance data and span related data (e.g. slow/frozen frames). */ @Nullable List stop(@NotNull ITransaction transaction); + /** Stops collecting performance data. */ + @Nullable + List stop(@NotNull String id); + /** Cancel the collector and stops it. Used on SDK close. */ @ApiStatus.Internal void close(); diff --git a/sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java similarity index 87% rename from sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java index 9489842b5c..ae99fe00c7 100644 --- a/sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java @@ -15,8 +15,7 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class DefaultTransactionPerformanceCollector - implements TransactionPerformanceCollector { +public final class DefaultCompositePerformanceCollector implements CompositePerformanceCollector { private static final long TRANSACTION_COLLECTION_INTERVAL_MILLIS = 100; private static final long TRANSACTION_COLLECTION_TIMEOUT_MILLIS = 30000; private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); @@ -31,7 +30,7 @@ public final class DefaultTransactionPerformanceCollector private final @NotNull AtomicBoolean isStarted = new AtomicBoolean(false); private long lastCollectionTimestamp = 0; - public DefaultTransactionPerformanceCollector(final @NotNull SentryOptions options) { + public DefaultCompositePerformanceCollector(final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "The options object is required."); this.snapshotCollectors = new ArrayList<>(); this.continuousCollectors = new ArrayList<>(); @@ -82,6 +81,23 @@ public void start(final @NotNull ITransaction transaction) { e); } } + start(transaction.getEventId().toString()); + } + + @Override + public void start(final @NotNull String id) { + if (hasNoCollectors) { + options + .getLogger() + .log( + SentryLevel.INFO, + "No collector found. Performance stats will not be captured during transactions."); + return; + } + + if (!performanceDataMap.containsKey(id)) { + performanceDataMap.put(id, new ArrayList<>()); + } if (!isStarted.getAndSet(true)) { try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { if (timer == null) { @@ -110,7 +126,7 @@ public void run() { // The timer is scheduled to run every 100ms on average. In case it takes longer, // subsequent tasks are executed more quickly. If two tasks are scheduled to run in // less than 10ms, the measurement that we collect is not meaningful, so we skip it - if (now - lastCollectionTimestamp < 10) { + if (now - lastCollectionTimestamp <= 10) { return; } lastCollectionTimestamp = now; @@ -157,14 +173,18 @@ public void onSpanFinished(@NotNull ISpan span) { transaction.getName(), transaction.getSpanContext().getTraceId().toString()); - final @Nullable List data = - performanceDataMap.remove(transaction.getEventId().toString()); - for (final @NotNull IPerformanceContinuousCollector collector : continuousCollectors) { collector.onSpanFinished(transaction); } - // close if they are no more remaining transactions + return stop(transaction.getEventId().toString()); + } + + @Override + public @Nullable List stop(final @NotNull String id) { + final @Nullable List data = performanceDataMap.remove(id); + + // close if they are no more running requests if (performanceDataMap.isEmpty()) { close(); } diff --git a/sentry/src/main/java/io/sentry/DefaultSpanFactory.java b/sentry/src/main/java/io/sentry/DefaultSpanFactory.java index 7ac2448849..6054ed5166 100644 --- a/sentry/src/main/java/io/sentry/DefaultSpanFactory.java +++ b/sentry/src/main/java/io/sentry/DefaultSpanFactory.java @@ -11,8 +11,8 @@ public final class DefaultSpanFactory implements ISpanFactory { final @NotNull TransactionContext context, final @NotNull IScopes scopes, final @NotNull TransactionOptions transactionOptions, - final @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { - return new SentryTracer(context, scopes, transactionOptions, transactionPerformanceCollector); + final @Nullable CompositePerformanceCollector compositePerformanceCollector) { + return new SentryTracer(context, scopes, transactionOptions, compositePerformanceCollector); } @Override diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index d71f5b10ba..537bcdf104 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -277,6 +277,16 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + Sentry.startProfiler(); + } + + @Override + public void stopProfiler() { + Sentry.stopProfiler(); + } + @Override public @NotNull SentryId captureProfileChunk( final @NotNull ProfileChunk profilingContinuousData) { diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 31c2270431..d6755c2c41 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -277,6 +277,16 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return scopes.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + scopes.startProfiler(); + } + + @Override + public void stopProfiler() { + scopes.stopProfiler(); + } + @ApiStatus.Internal @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java index c94eb9bba3..14ce41a815 100644 --- a/sentry/src/main/java/io/sentry/IContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -12,8 +13,9 @@ public interface IContinuousProfiler { void stop(); - void setScopes(final @NotNull IScopes scopes); - /** Cancel the profiler and stops it. Used on SDK close. */ void close(); + + @NotNull + SentryId getProfilerId(); } diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index 14b8753b28..59a577f04b 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -592,6 +592,10 @@ ITransaction startTransaction( final @NotNull TransactionContext transactionContext, final @NotNull TransactionOptions transactionOptions); + void startProfiler(); + + void stopProfiler(); + /** * Associates {@link ISpan} and the transaction name with the {@link Throwable}. Used to determine * in which trace the exception has been thrown in framework integrations. diff --git a/sentry/src/main/java/io/sentry/ISpanFactory.java b/sentry/src/main/java/io/sentry/ISpanFactory.java index 1e429e2fea..9b7c6afaba 100644 --- a/sentry/src/main/java/io/sentry/ISpanFactory.java +++ b/sentry/src/main/java/io/sentry/ISpanFactory.java @@ -11,7 +11,7 @@ ITransaction createTransaction( @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector); + @Nullable CompositePerformanceCollector compositePerformanceCollector); @NotNull ISpan createSpan( diff --git a/sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java similarity index 51% rename from sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java index abf5ec5f6a..a159be9182 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java @@ -4,20 +4,23 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class NoOpTransactionPerformanceCollector implements TransactionPerformanceCollector { +public final class NoOpCompositePerformanceCollector implements CompositePerformanceCollector { - private static final NoOpTransactionPerformanceCollector instance = - new NoOpTransactionPerformanceCollector(); + private static final NoOpCompositePerformanceCollector instance = + new NoOpCompositePerformanceCollector(); - public static NoOpTransactionPerformanceCollector getInstance() { + public static NoOpCompositePerformanceCollector getInstance() { return instance; } - private NoOpTransactionPerformanceCollector() {} + private NoOpCompositePerformanceCollector() {} @Override public void start(@NotNull ITransaction transaction) {} + @Override + public void start(@NotNull String id) {} + @Override public void onSpanStarted(@NotNull ISpan span) {} @@ -29,6 +32,11 @@ public void onSpanFinished(@NotNull ISpan span) {} return null; } + @Override + public @Nullable List stop(@NotNull String id) { + return null; + } + @Override public void close() {} } diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java index b17123029f..4ccf7cc681 100644 --- a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.NotNull; public final class NoOpContinuousProfiler implements IContinuousProfiler { @@ -18,9 +19,6 @@ public void start() {} @Override public void stop() {} - @Override - public void setScopes(@NotNull IScopes scopes) {} - @Override public boolean isRunning() { return false; @@ -28,4 +26,9 @@ public boolean isRunning() { @Override public void close() {} + + @Override + public @NotNull SentryId getProfilerId() { + return SentryId.EMPTY_ID; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index a304619b27..925f1e64ee 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -243,6 +243,12 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return NoOpTransaction.getInstance(); } + @Override + public void startProfiler() {} + + @Override + public void stopProfiler() {} + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 9ec3db2a62..11bae042b0 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -238,6 +238,12 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return NoOpTransaction.getInstance(); } + @Override + public void startProfiler() {} + + @Override + public void stopProfiler() {} + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/NoOpSpanFactory.java b/sentry/src/main/java/io/sentry/NoOpSpanFactory.java index 05bea4edfe..871e281054 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpanFactory.java +++ b/sentry/src/main/java/io/sentry/NoOpSpanFactory.java @@ -20,7 +20,7 @@ public static NoOpSpanFactory getInstance() { @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + @Nullable CompositePerformanceCollector compositePerformanceCollector) { return NoOpTransaction.getInstance(); } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 44cb921209..725c151dbd 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -160,7 +160,7 @@ public Builder( final @NotNull File traceFile) { this.profilerId = profilerId; this.chunkId = chunkId; - this.measurements = measurements; + this.measurements = new ConcurrentHashMap<>(measurements); this.traceFile = traceFile; } diff --git a/sentry/src/main/java/io/sentry/ProfileContext.java b/sentry/src/main/java/io/sentry/ProfileContext.java index 3a7fa9af2f..e4b411c279 100644 --- a/sentry/src/main/java/io/sentry/ProfileContext.java +++ b/sentry/src/main/java/io/sentry/ProfileContext.java @@ -1,6 +1,5 @@ package io.sentry; -import com.jakewharton.nopen.annotation.Open; import io.sentry.protocol.SentryId; import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; @@ -11,8 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -@Open -public class ProfileContext implements JsonUnknown, JsonSerializable { +public final class ProfileContext implements JsonUnknown, JsonSerializable { public static final String TYPE = "profile"; /** Determines which trace the Span belongs to. */ diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 4b7478a3f4..86f0f5f8bc 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -26,7 +26,7 @@ public final class Scopes implements IScopes { private final @Nullable Scopes parentScopes; private final @NotNull String creator; - private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; + private final @NotNull CompositePerformanceCollector compositePerformanceCollector; private final @NotNull CombinedScopeView combinedScope; @@ -53,7 +53,7 @@ private Scopes( final @NotNull SentryOptions options = getOptions(); validateOptions(options); - this.transactionPerformanceCollector = options.getTransactionPerformanceCollector(); + this.compositePerformanceCollector = options.getCompositePerformanceCollector(); } public @NotNull String getCreator() { @@ -404,7 +404,8 @@ public void close(final boolean isRestarting) { configureScope(scope -> scope.clear()); configureScope(ScopeType.ISOLATION, scope -> scope.clear()); getOptions().getTransactionProfiler().close(); - getOptions().getTransactionPerformanceCollector().close(); + getOptions().getContinuousProfiler().close(); + getOptions().getCompositePerformanceCollector().close(); final @NotNull ISentryExecutorService executorService = getOptions().getExecutorService(); if (isRestarting) { executorService.submit( @@ -897,10 +898,10 @@ public void flush(long timeoutMillis) { transaction = spanFactory.createTransaction( - transactionContext, this, transactionOptions, transactionPerformanceCollector); + transactionContext, this, transactionOptions, compositePerformanceCollector); // new SentryTracer( // transactionContext, this, transactionOptions, - // transactionPerformanceCollector); + // compositePerformanceCollector); // The listener is called only if the transaction exists, as the transaction is needed to // stop it @@ -922,6 +923,34 @@ public void flush(long timeoutMillis) { return transaction; } + @Override + public void startProfiler() { + if (getOptions().isContinuousProfilingEnabled()) { + getOptions().getLogger().log(SentryLevel.DEBUG, "Started continuous Profiling."); + getOptions().getContinuousProfiler().start(); + } else { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it."); + } + } + + @Override + public void stopProfiler() { + if (getOptions().isContinuousProfilingEnabled()) { + getOptions().getLogger().log(SentryLevel.DEBUG, "Stopped continuous Profiling."); + getOptions().getContinuousProfiler().stop(); + } else { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it."); + } + } + @Override @ApiStatus.Internal public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 8da3c4f615..9944a87a0a 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -280,6 +280,16 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + Sentry.startProfiler(); + } + + @Override + public void stopProfiler() { + Sentry.stopProfiler(); + } + @ApiStatus.Internal @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 7a35720e2d..3c30597b92 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1049,6 +1049,16 @@ public static void endSession() { return getCurrentScopes().startTransaction(transactionContext, transactionOptions); } + /** Starts the continuous profiler, if enabled. */ + public static void startProfiler() { + getCurrentScopes().startProfiler(); + } + + /** Starts the continuous profiler, if enabled. */ + public static void stopProfiler() { + getCurrentScopes().stopProfiler(); + } + /** * Gets the current active transaction or span. * diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index a9828792d7..b0926b9d93 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -18,6 +18,7 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe @Nullable Double traceSampleRate; @Nullable String profilingTracesDirPath; boolean isProfilingEnabled; + boolean isContinuousProfilingEnabled; int profilingTracesHz; private @Nullable Map unknown; @@ -30,6 +31,7 @@ public SentryAppStartProfilingOptions() { profileSampleRate = null; profilingTracesDirPath = null; isProfilingEnabled = false; + isContinuousProfilingEnabled = false; profilingTracesHz = 0; } @@ -42,6 +44,7 @@ public SentryAppStartProfilingOptions() { profileSampleRate = samplingDecision.getProfileSampleRate(); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); + isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); profilingTracesHz = options.getProfilingTracesHz(); } @@ -93,6 +96,14 @@ public boolean isProfilingEnabled() { return isProfilingEnabled; } + public void setContinuousProfilingEnabled(final boolean continuousProfilingEnabled) { + isContinuousProfilingEnabled = continuousProfilingEnabled; + } + + public boolean isContinuousProfilingEnabled() { + return isContinuousProfilingEnabled; + } + public void setProfilingTracesHz(final int profilingTracesHz) { this.profilingTracesHz = profilingTracesHz; } @@ -110,6 +121,7 @@ public static final class JsonKeys { public static final String TRACE_SAMPLE_RATE = "trace_sample_rate"; public static final String PROFILING_TRACES_DIR_PATH = "profiling_traces_dir_path"; public static final String IS_PROFILING_ENABLED = "is_profiling_enabled"; + public static final String IS_CONTINUOUS_PROFILING_ENABLED = "is_continuous_profiling_enabled"; public static final String PROFILING_TRACES_HZ = "profiling_traces_hz"; } @@ -123,6 +135,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.TRACE_SAMPLE_RATE).value(logger, traceSampleRate); writer.name(JsonKeys.PROFILING_TRACES_DIR_PATH).value(logger, profilingTracesDirPath); writer.name(JsonKeys.IS_PROFILING_ENABLED).value(logger, isProfilingEnabled); + writer + .name(JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED) + .value(logger, isContinuousProfilingEnabled); writer.name(JsonKeys.PROFILING_TRACES_HZ).value(logger, profilingTracesHz); if (unknown != null) { @@ -195,6 +210,12 @@ public static final class Deserializer options.isProfilingEnabled = isProfilingEnabled; } break; + case JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED: + Boolean isContinuousProfilingEnabled = reader.nextBooleanOrNull(); + if (isContinuousProfilingEnabled != null) { + options.isContinuousProfilingEnabled = isContinuousProfilingEnabled; + } + break; case JsonKeys.PROFILING_TRACES_HZ: Integer profilingTracesHz = reader.nextIntegerOrNull(); if (profilingTracesHz != null) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index c02765fd54..9c914eda4f 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -353,9 +353,12 @@ public class SentryOptions { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; - /** Listener interface to perform operations when a transaction is started or ended */ + /** Profiler that runs when a transaction is started until it's finished. */ private @NotNull ITransactionProfiler transactionProfiler = NoOpTransactionProfiler.getInstance(); + /** Profiler that runs continuously until stopped. */ + private @NotNull IContinuousProfiler continuousProfiler = NoOpContinuousProfiler.getInstance(); + /** * Contains a list of origins to which `sentry-trace` header should be sent in HTTP integrations. */ @@ -427,8 +430,8 @@ public class SentryOptions { private final @NotNull List performanceCollectors = new ArrayList<>(); /** Performance collector that collect performance stats while transactions run. */ - private @NotNull TransactionPerformanceCollector transactionPerformanceCollector = - NoOpTransactionPerformanceCollector.getInstance(); + private @NotNull CompositePerformanceCollector compositePerformanceCollector = + NoOpCompositePerformanceCollector.getInstance(); /** Enables the time-to-full-display spans in navigation transactions. */ private boolean enableTimeToFullDisplayTracing = false; @@ -1664,6 +1667,28 @@ public void setTransactionProfiler(final @Nullable ITransactionProfiler transact } } + /** + * Returns the continuous profiler. + * + * @return the continuous profiler. + */ + public @NotNull IContinuousProfiler getContinuousProfiler() { + return continuousProfiler; + } + + /** + * Sets the continuous profiler. It only has effect if no profiler was already set. + * + * @param continuousProfiler - the continuous profiler + */ + public void setContinuousProfiler(final @Nullable IContinuousProfiler continuousProfiler) { + // We allow to set the profiler only if it was not set before, and we don't allow to unset it. + if (this.continuousProfiler == NoOpContinuousProfiler.getInstance() + && continuousProfiler != null) { + this.continuousProfiler = continuousProfiler; + } + } + /** * Returns if profiling is enabled for transactions. * @@ -1674,6 +1699,17 @@ public boolean isProfilingEnabled() { || getProfilesSampler() != null; } + /** + * Returns if continuous profiling is enabled. This means that no profile sample rate has been + * set. + * + * @return if continuous profiling is enabled. + */ + @ApiStatus.Internal + public boolean isContinuousProfilingEnabled() { + return getProfilesSampleRate() == null && getProfilesSampler() == null; + } + /** * Returns the callback used to determine if a profile is sampled. * @@ -1997,24 +2033,24 @@ public void setThreadChecker(final @NotNull IThreadChecker threadChecker) { } /** - * Gets the performance collector used to collect performance stats while transactions run. + * Gets the performance collector used to collect performance stats in a time period. * * @return the performance collector. */ @ApiStatus.Internal - public @NotNull TransactionPerformanceCollector getTransactionPerformanceCollector() { - return transactionPerformanceCollector; + public @NotNull CompositePerformanceCollector getCompositePerformanceCollector() { + return compositePerformanceCollector; } /** - * Sets the performance collector used to collect performance stats while transactions run. + * Sets the performance collector used to collect performance stats in a time period. * - * @param transactionPerformanceCollector the performance collector. + * @param compositePerformanceCollector the performance collector. */ @ApiStatus.Internal - public void setTransactionPerformanceCollector( - final @NotNull TransactionPerformanceCollector transactionPerformanceCollector) { - this.transactionPerformanceCollector = transactionPerformanceCollector; + public void setCompositePerformanceCollector( + final @NotNull CompositePerformanceCollector compositePerformanceCollector) { + this.compositePerformanceCollector = compositePerformanceCollector; } /** diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 65901a2e1a..943dc349f2 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -7,6 +7,7 @@ import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; +import io.sentry.util.thread.IThreadChecker; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; @@ -50,7 +51,7 @@ public final class SentryTracer implements ITransaction { private @NotNull TransactionNameSource transactionNameSource; private final @NotNull Instrumenter instrumenter; private final @NotNull Contexts contexts = new Contexts(); - private final @Nullable TransactionPerformanceCollector transactionPerformanceCollector; + private final @Nullable CompositePerformanceCollector compositePerformanceCollector; private final @NotNull TransactionOptions transactionOptions; public SentryTracer(final @NotNull TransactionContext context, final @NotNull IScopes scopes) { @@ -68,7 +69,7 @@ public SentryTracer( final @NotNull TransactionContext context, final @NotNull IScopes scopes, final @NotNull TransactionOptions transactionOptions, - final @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + final @Nullable CompositePerformanceCollector compositePerformanceCollector) { Objects.requireNonNull(context, "context is required"); Objects.requireNonNull(scopes, "scopes are required"); @@ -77,7 +78,7 @@ public SentryTracer( this.name = context.getName(); this.instrumenter = context.getInstrumenter(); this.scopes = scopes; - this.transactionPerformanceCollector = transactionPerformanceCollector; + this.compositePerformanceCollector = compositePerformanceCollector; this.transactionNameSource = context.getTransactionNameSource(); this.transactionOptions = transactionOptions; @@ -87,10 +88,16 @@ public SentryTracer( this.baggage = new Baggage(scopes.getOptions().getLogger()); } + final @NotNull SentryId continuousProfilerId = + scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!continuousProfilerId.equals(SentryId.EMPTY_ID)) { + this.contexts.setProfile(new ProfileContext(continuousProfilerId)); + } + // We are currently sending the performance data only in profiles, but we are always sending // performance measurements. - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.start(this); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.start(this); } if (transactionOptions.getIdleTimeout() != null @@ -220,8 +227,8 @@ public void finish( finishedCallback.execute(this); } - if (transactionPerformanceCollector != null) { - performanceCollectionData.set(transactionPerformanceCollector.stop(this)); + if (compositePerformanceCollector != null) { + performanceCollectionData.set(compositePerformanceCollector.stop(this)); } }); @@ -469,8 +476,8 @@ private ISpan createChild( spanContext, spanOptions, finishingSpan -> { - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.onSpanFinished(finishingSpan); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.onSpanFinished(finishingSpan); } final FinishStatus finishStatus = this.finishStatus; if (transactionOptions.getIdleTimeout() != null) { @@ -495,8 +502,8 @@ private ISpan createChild( // timestamp, // spanOptions, // finishingSpan -> { - // if (transactionPerformanceCollector != null) { - // transactionPerformanceCollector.onSpanFinished(finishingSpan); + // if (compositePerformanceCollector != null) { + // compositePerformanceCollector.onSpanFinished(finishingSpan); // } // final FinishStatus finishStatus = this.finishStatus; // if (transactionOptions.getIdleTimeout() != null) { @@ -513,16 +520,17 @@ private ISpan createChild( // } // }); // span.setDescription(description); - final long threadId = scopes.getOptions().getThreadChecker().currentThreadSystemId(); - span.setData(SpanDataConvention.THREAD_ID, String.valueOf(threadId)); + final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); + final SentryId profilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!profilerId.equals(SentryId.EMPTY_ID)) { + span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); + } span.setData( - SpanDataConvention.THREAD_NAME, - scopes.getOptions().getThreadChecker().isMainThread() - ? "main" - : Thread.currentThread().getName()); + SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); + span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); this.children.add(span); - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.onSpanStarted(span); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.onSpanStarted(span); } return span; } else { diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index f012233504..0a9a143616 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -4,6 +4,7 @@ import io.sentry.protocol.SentryId; import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; +import io.sentry.util.thread.IThreadChecker; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -95,6 +96,11 @@ public SpanContext( this.description = description; this.status = status; this.origin = origin; + final IThreadChecker threadChecker = + ScopesAdapter.getInstance().getOptions().getThreadChecker(); + this.data.put( + SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); + this.data.put(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); } /** @@ -120,6 +126,11 @@ public SpanContext(final @NotNull SpanContext spanContext) { if (copiedUnknown != null) { this.unknown = copiedUnknown; } + this.baggage = spanContext.baggage; + final Map copiedData = CollectionUtils.newConcurrentHashMap(spanContext.data); + if (copiedData != null) { + this.data = copiedData; + } } public void setOperation(final @NotNull String operation) { diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index ffe2414af3..c4329f6dca 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -25,4 +25,5 @@ public interface SpanDataConvention { String CONTRIBUTES_TTFD = "ui.contributes_to_ttfd"; String HTTP_START_TIMESTAMP = "http.start_timestamp"; String HTTP_END_TIMESTAMP = "http.end_timestamp"; + String PROFILER_ID = "profiler_id"; } diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index 684001843a..d2a9e5140b 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -78,7 +78,7 @@ public SentryTransaction(final @NotNull SentryTracer sentryTracer) { final SpanContext tracerContext = sentryTracer.getSpanContext(); Map data = sentryTracer.getData(); // tags must be placed on the root of the transaction instead of contexts.trace.tags - final SpanContext tracerContextToSend = + final @NotNull SpanContext tracerContextToSend = new SpanContext( tracerContext.getTraceId(), tracerContext.getSpanId(), diff --git a/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java index 81af056e71..deea360f8c 100644 --- a/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java @@ -32,6 +32,14 @@ public interface IThreadChecker { */ boolean isMainThread(final @NotNull SentryThread sentryThread); + /** + * Returns the name of the current thread + * + * @return the name of the current thread + */ + @NotNull + String getCurrentThreadName(); + /** * Returns the system id of the current thread. Currently only used for Android. * diff --git a/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java index b1497d17e7..f80a996785 100644 --- a/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java @@ -33,6 +33,11 @@ public boolean isMainThread(@NotNull SentryThread sentryThread) { return false; } + @Override + public @NotNull String getCurrentThreadName() { + return ""; + } + @Override public long currentThreadSystemId() { return 0; diff --git a/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java index bfa8aac139..2f9b6fc1d2 100644 --- a/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java @@ -44,6 +44,11 @@ public boolean isMainThread(final @NotNull SentryThread sentryThread) { return threadId != null && isMainThread(threadId); } + @Override + public @NotNull String getCurrentThreadName() { + return Thread.currentThread().getName(); + } + @Override public long currentThreadSystemId() { return Thread.currentThread().getId(); diff --git a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt index db113fa009..20d6b69367 100644 --- a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt @@ -28,7 +28,10 @@ class CheckInSerializationTest { it.traceId = SentryId("f382e3180c714217a81371f8c644aefe") it.spanId = SpanId("85694b9f567145a6") } - ) + ).apply { + data[SpanDataConvention.THREAD_ID] = 10 + data[SpanDataConvention.THREAD_NAME] = "test" + } ) duration = 12.3 environment = "env" diff --git a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt similarity index 83% rename from sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt rename to sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index 60005935c9..fe9dd6039d 100644 --- a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -23,9 +23,9 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class DefaultTransactionPerformanceCollectorTest { +class DefaultCompositePerformanceCollectorTest { - private val className = "io.sentry.DefaultTransactionPerformanceCollector" + private val className = "io.sentry.DefaultCompositePerformanceCollector" private val ctorTypes: Array> = arrayOf(SentryOptions::class.java) private val fixture = Fixture() private val threadChecker = ThreadChecker.getInstance() @@ -33,6 +33,7 @@ class DefaultTransactionPerformanceCollectorTest { private class Fixture { lateinit var transaction1: ITransaction lateinit var transaction2: ITransaction + val id1 = "id1" val scopes: IScopes = mock() val options = SentryOptions() var mockTimer: Timer? = null @@ -50,7 +51,7 @@ class DefaultTransactionPerformanceCollectorTest { whenever(scopes.options).thenReturn(options) } - fun getSut(memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, executorService: ISentryExecutorService = deferredExecutorService): TransactionPerformanceCollector { + fun getSut(memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, executorService: ISentryExecutorService = deferredExecutorService): CompositePerformanceCollector { options.dsn = "https://key@sentry.io/proj" options.executorService = executorService if (cpuCollector != null) { @@ -61,7 +62,7 @@ class DefaultTransactionPerformanceCollectorTest { } transaction1 = SentryTracer(TransactionContext("", ""), scopes) transaction2 = SentryTracer(TransactionContext("", ""), scopes) - val collector = DefaultTransactionPerformanceCollector(options) + val collector = DefaultCompositePerformanceCollector(options) val timer: Timer = collector.getProperty("timer") ?: Timer(true) mockTimer = spy(timer) collector.injectForField("timer", mockTimer) @@ -104,6 +105,13 @@ class DefaultTransactionPerformanceCollectorTest { verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) } + @Test + fun `when start with a string, timer is scheduled every 100 milliseconds`() { + val collector = fixture.getSut() + collector.start(fixture.id1) + verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + } + @Test fun `when stop, timer is stopped`() { val collector = fixture.getSut() @@ -113,6 +121,15 @@ class DefaultTransactionPerformanceCollectorTest { verify(fixture.mockTimer)!!.cancel() } + @Test + fun `when stop with a string, timer is stopped`() { + val collector = fixture.getSut() + collector.start(fixture.id1) + collector.stop(fixture.id1) + verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.cancel() + } + @Test fun `stopping a not collected transaction return null`() { val collector = fixture.getSut() @@ -122,34 +139,53 @@ class DefaultTransactionPerformanceCollectorTest { assertNull(data) } + @Test + fun `stopping a not collected id return null`() { + val collector = fixture.getSut() + val data = collector.stop(fixture.id1) + verify(fixture.mockTimer, never())!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer, never())!!.cancel() + assertNull(data) + } + @Test fun `collector collect memory for multiple transactions`() { val collector = fixture.getSut() collector.start(fixture.transaction1) collector.start(fixture.transaction2) + collector.start(fixture.id1) // Let's sleep to make the collector get values Thread.sleep(300) val data1 = collector.stop(fixture.transaction1) - // There is still a transaction running: the timer shouldn't stop now + // There is still a transaction and an id running: the timer shouldn't stop now verify(fixture.mockTimer, never())!!.cancel() val data2 = collector.stop(fixture.transaction2) - // There are no more transactions running: the time should stop now + // There is still an id running: the timer shouldn't stop now + verify(fixture.mockTimer, never())!!.cancel() + + val data3 = collector.stop(fixture.id1) + // There are no more transactions or ids running: the time should stop now verify(fixture.mockTimer)!!.cancel() assertNotNull(data1) assertNotNull(data2) + assertNotNull(data3) val memoryData1 = data1.map { it.memoryData } val cpuData1 = data1.map { it.cpuData } val memoryData2 = data2.map { it.memoryData } val cpuData2 = data2.map { it.cpuData } + val memoryData3 = data3.map { it.memoryData } + val cpuData3 = data3.map { it.cpuData } // The data returned by the collector is not empty assertFalse(memoryData1.isEmpty()) assertFalse(cpuData1.isEmpty()) assertFalse(memoryData2.isEmpty()) assertFalse(cpuData2.isEmpty()) + assertFalse(memoryData3.isEmpty()) + assertFalse(cpuData3.isEmpty()) } @Test @@ -266,6 +302,27 @@ class DefaultTransactionPerformanceCollectorTest { verify(collector).clear() } + @Test + fun `Continuous collectors are not called when collecting using a string id`() { + val collector = mock() + fixture.options.performanceCollectors.add(collector) + val sut = fixture.getSut(memoryCollector = null, cpuCollector = null) + + // when a collection is started with an id + sut.start(fixture.id1) + + // collector should not be notified + verify(collector, never()).onSpanStarted(fixture.transaction1) + + // when the id collection is stopped + sut.stop(fixture.id1) + + // collector should not be notified + verify(collector, never()).onSpanFinished(fixture.transaction1) + + verify(collector).clear() + } + @Test fun `Continuous collectors are notified properly even when multiple txn are running`() { val collector = mock() diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index b48ae79703..310c153709 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -265,4 +265,14 @@ class HubAdapterTest { HubAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfiler calls Hub`() { + HubAdapter.getInstance().startProfiler() + verify(scopes).startProfiler() + } + + @Test fun `stopProfiler calls Hub`() { + HubAdapter.getInstance().stopProfiler() + verify(scopes).stopProfiler() + } } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 2128797261..53dd7d85bf 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1068,6 +1068,7 @@ class JsonSerializerTest { trace.status = SpanStatus.OK trace.setTag("myTag", "myValue") trace.sampled = true + trace.data["dataKey"] = "dataValue" val tracer = SentryTracer(trace, fixture.scopes) tracer.setData("dataKey", "dataValue") val span = tracer.startChild("child") @@ -1101,6 +1102,9 @@ class JsonSerializerTest { assertEquals("dataValue", (jsonTrace["data"] as Map<*, *>)["dataKey"] as String) assertNotNull(jsonTrace["trace_id"] as String) assertNotNull(jsonTrace["span_id"] as String) + assertNotNull(jsonTrace["data"] as Map<*, *>) { + assertEquals("dataValue", it["dataKey"]) + } assertEquals("http", jsonTrace["op"] as String) assertEquals("some request", jsonTrace["description"] as String) assertEquals("ok", jsonTrace["status"] as String) @@ -1163,7 +1167,7 @@ class JsonSerializerTest { assertEquals("0a53026963414893", transaction.contexts.trace!!.spanId.toString()) assertEquals("http", transaction.contexts.trace!!.operation) assertNotNull(transaction.contexts["custom"]) - assertEquals("transactionDataValue", transaction.contexts.trace!!.data!!["transactionDataKey"]) + assertEquals("transactionDataValue", transaction.contexts.trace!!.data["transactionDataKey"]) assertEquals("some-value", (transaction.contexts["custom"] as Map<*, *>)["some-key"]) assertEquals("extraValue", transaction.getExtra("extraKey")) @@ -1226,7 +1230,8 @@ class JsonSerializerTest { val actual = serializeToString(appStartProfilingOptions) val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"trace_sampled\":false," + - "\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false,\"profiling_traces_hz\":65}" + "\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"is_continuous_profiling_enabled\":false,\"profiling_traces_hz\":65}" assertEquals(expected, actual) } @@ -1243,6 +1248,7 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.profileSampled, actual.profileSampled) assertEquals(appStartProfilingOptions.profileSampleRate, actual.profileSampleRate) assertEquals(appStartProfilingOptions.isProfilingEnabled, actual.isProfilingEnabled) + assertEquals(appStartProfilingOptions.isContinuousProfilingEnabled, actual.isContinuousProfilingEnabled) assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) assertEquals(appStartProfilingOptions.profilingTracesDirPath, actual.profilingTracesDirPath) assertNull(actual.unknown) @@ -1546,6 +1552,7 @@ class JsonSerializerTest { profileSampled = true profileSampleRate = 0.8 isProfilingEnabled = false + isContinuousProfilingEnabled = false profilingTracesHz = 65 } diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt index afbce4a8cb..e791651aef 100644 --- a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -1,6 +1,8 @@ package io.sentry +import io.sentry.protocol.SentryId import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse class NoOpContinuousProfilerTest { @@ -22,4 +24,9 @@ class NoOpContinuousProfilerTest { @Test fun `close does not throw`() = profiler.close() + + @Test + fun `getProfilerId returns Empty SentryId`() { + assertEquals(profiler.profilerId, SentryId.EMPTY_ID) + } } diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index fbd6e4c41b..e0eb08ded0 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -115,4 +115,10 @@ class NoOpHubTest { sut.withScope(scopeCallback) verify(scopeCallback).run(NoOpScope.getInstance()) } + + @Test + fun `startProfiler doesnt throw`() = sut.startProfiler() + + @Test + fun `stopProfiler doesnt throw`() = sut.stopProfiler() } diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 8a1850e7dd..c8039551ff 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -38,6 +38,7 @@ class OutboxSenderTest { whenever(options.dsn).thenReturn("https://key@sentry.io/proj") whenever(options.dateProvider).thenReturn(SentryNanotimeDateProvider()) whenever(options.threadChecker).thenReturn(NoOpThreadChecker.getInstance()) + whenever(options.continuousProfiler).thenReturn(NoOpContinuousProfiler.getInstance()) whenever(scopes.options).thenReturn(this.options) } diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index d30b653456..9c7418c6b5 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -265,4 +265,14 @@ class ScopesAdapterTest { ScopesAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfiler calls Scopes`() { + ScopesAdapter.getInstance().startProfiler() + verify(scopes).startProfiler() + } + + @Test fun `stopProfiler calls Scopes`() { + ScopesAdapter.getInstance().stopProfiler() + verify(scopes).stopProfiler() + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 04473ddf7a..68a1fee5d6 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1795,18 +1795,21 @@ class ScopesTest { fun `Scopes should close the sentry executor processor, profiler and performance collector on close call`() { val executor = mock() val profiler = mock() - val performanceCollector = mock() + val continuousProfiler = mock() + val performanceCollector = mock() val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" cacheDirPath = file.absolutePath executorService = executor setTransactionProfiler(profiler) - transactionPerformanceCollector = performanceCollector + compositePerformanceCollector = performanceCollector + setContinuousProfiler(continuousProfiler) } val sut = createScopes(options) sut.close() verify(executor).close(any()) verify(profiler).close() + verify(continuousProfiler).close() verify(performanceCollector).close() } @@ -2157,6 +2160,56 @@ class ScopesTest { assertEquals("other.span.origin", transaction.spanContext.origin) } + @Test + fun `startProfiler starts the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + } + scopes.startProfiler() + verify(profiler).start() + } + + @Test + fun `stopProfiler stops the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + } + scopes.stopProfiler() + verify(profiler).stop() + } + + @Test + fun `startProfiler logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.startProfiler() + verify(profiler, never()).start() + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + + @Test + fun `stopProfiler logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.stopProfiler() + verify(profiler, never()).stop() + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateScopes(optionsConfiguration: Sentry.OptionsConfiguration? = null): IScopes { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 21f33383e7..74ce882fe1 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -193,42 +193,47 @@ class SentryOptionsTest { } @Test - fun `when options is initialized, isProfilingEnabled is false`() { + fun `when options is initialized, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { assertFalse(SentryOptions().isProfilingEnabled) + assertTrue(SentryOptions().isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false`() { + fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { val options = SentryOptions().apply { this.profilesSampleRate = null this.profilesSampler = null } assertFalse(options.isProfilingEnabled) + assertTrue(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is 0 and profilesSampler is null, isProfilingEnabled is false`() { + fun `when profilesSampleRate is 0 and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampleRate = 0.0 this.profilesSampler = null } assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is set to a value higher than 0, isProfilingEnabled is true`() { + fun `when profilesSampleRate is set to a value higher than 0, isProfilingEnabled is true and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampleRate = 0.1 } assertTrue(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampler is set to a value, isProfilingEnabled is true`() { + fun `when profilesSampler is set to a value, isProfilingEnabled is true and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampler = SentryOptions.ProfilesSamplerCallback { 1.0 } } assertTrue(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test @@ -250,8 +255,8 @@ class SentryOptionsTest { } @Test - fun `when options is initialized, transactionPerformanceCollector is set`() { - assertIs(SentryOptions().transactionPerformanceCollector) + fun `when options is initialized, compositePerformanceCollector is set`() { + assertIs(SentryOptions().compositePerformanceCollector) } @Test @@ -259,6 +264,11 @@ class SentryOptionsTest { assert(SentryOptions().transactionProfiler == NoOpTransactionProfiler.getInstance()) } + @Test + fun `when options is initialized, continuousProfiler is noop`() { + assert(SentryOptions().continuousProfiler == NoOpContinuousProfiler.getInstance()) + } + @Test fun `when options is initialized, collector is empty list`() { assertTrue(SentryOptions().performanceCollectors.isEmpty()) @@ -466,16 +476,16 @@ class SentryOptionsTest { } @Test - fun `when options are initialized, TransactionPerformanceCollector is a NoOp`() { - assertEquals(SentryOptions().transactionPerformanceCollector, NoOpTransactionPerformanceCollector.getInstance()) + fun `when options are initialized, CompositePerformanceCollector is a NoOp`() { + assertEquals(SentryOptions().compositePerformanceCollector, NoOpCompositePerformanceCollector.getInstance()) } @Test - fun `when setTransactionPerformanceCollector is called, overrides default`() { - val performanceCollector = mock() + fun `when setCompositePerformanceCollector is called, overrides default`() { + val performanceCollector = mock() val options = SentryOptions() - options.transactionPerformanceCollector = performanceCollector - assertEquals(performanceCollector, options.transactionPerformanceCollector) + options.compositePerformanceCollector = performanceCollector + assertEquals(performanceCollector, options.compositePerformanceCollector) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 5c707a9893..6f7cd427da 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1277,6 +1277,52 @@ class SentryTest { assertNotSame(s1, s2) } + @Test + fun `startProfiler starts the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + } + Sentry.startProfiler() + verify(profiler).start() + } + + @Test + fun `startProfiler is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.startProfiler() + verify(profiler, never()).start() + } + + @Test + fun `stopProfiler stops the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + } + Sentry.stopProfiler() + verify(profiler).stop() + } + + @Test + fun `stopProfiler is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.stopProfiler() + verify(profiler, never()).stop() + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set @@ -1328,6 +1374,7 @@ class SentryTest { override fun isMainThread(): Boolean = false override fun isMainThread(sentryThread: SentryThread): Boolean = false override fun currentThreadSystemId(): Long = 0 + override fun getCurrentThreadName(): String = "" } private class CustomMemoryCollector : diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 3d5eca7f48..2be549cba6 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -32,14 +32,18 @@ class SentryTracerTest { private class Fixture { val options = SentryOptions() val scopes: Scopes - val transactionPerformanceCollector: TransactionPerformanceCollector + val compositePerformanceCollector: CompositePerformanceCollector init { options.dsn = "https://key@sentry.io/proj" options.environment = "environment" options.release = "release@3.0.0" scopes = spy(createTestScopes(options)) - transactionPerformanceCollector = spy(DefaultTransactionPerformanceCollector(options)) + compositePerformanceCollector = spy( + DefaultCompositePerformanceCollector( + options + ) + ) } fun getSut( @@ -51,7 +55,7 @@ class SentryTracerTest { trimEnd: Boolean = false, transactionFinishedCallback: TransactionFinishedCallback? = null, samplingDecision: TracesSamplingDecision? = null, - performanceCollector: TransactionPerformanceCollector? = transactionPerformanceCollector + performanceCollector: CompositePerformanceCollector? = compositePerformanceCollector ): SentryTracer { optionsConfiguration.configure(options) @@ -209,6 +213,65 @@ class SentryTracerTest { verify(transactionProfiler).onTransactionFinish(any(), anyOrNull(), anyOrNull()) } + @Test + fun `when continuous profiler is running, profile context is set`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + }) + tracer.finish() + verify(fixture.scopes).captureTransaction( + check { + assertNotNull(it.contexts.profile) { + assertEquals(profilerId, it.profilerId) + } + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is not running, profile context is not set`() { + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(NoOpContinuousProfiler.getInstance()) + }) + tracer.finish() + verify(fixture.scopes).captureTransaction( + check { + assertNull(it.contexts.profile) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is running, profiler id is set in span data`() { + val profilerId = SentryId() + val profiler = mock() + whenever(profiler.profilerId).thenReturn(profilerId) + + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(profiler) + }) + val span = tracer.startChild("span.op") + assertEquals(profilerId.toString(), span.getData(SpanDataConvention.PROFILER_ID)) + } + + @Test + fun `when continuous profiler is not running, profiler id is not set in span data`() { + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(NoOpContinuousProfiler.getInstance()) + }) + val span = tracer.startChild("span.op") + assertNull(span.getData(SpanDataConvention.PROFILER_ID)) + } + @Test fun `when transaction is finished, transaction is cleared from the scope`() { val tracer = fixture.getSut() @@ -1026,35 +1089,35 @@ class SentryTracerTest { } @Test - fun `when transaction is created, but not profiled, transactionPerformanceCollector is started anyway`() { + fun `when transaction is created, but not profiled, compositePerformanceCollector is started anyway`() { val transaction = fixture.getSut() - verify(fixture.transactionPerformanceCollector).start(anyOrNull()) + verify(fixture.compositePerformanceCollector).start(anyOrNull()) } @Test - fun `when transaction is created and profiled transactionPerformanceCollector is started`() { + fun `when transaction is created and profiled compositePerformanceCollector is started`() { val transaction = fixture.getSut(optionsConfiguration = { it.profilesSampleRate = 1.0 }, samplingDecision = TracesSamplingDecision(true, null, true, null)) - verify(fixture.transactionPerformanceCollector).start(check { assertEquals(transaction, it) }) + verify(fixture.compositePerformanceCollector).start(check { assertEquals(transaction, it) }) } @Test - fun `when transaction is finished, transactionPerformanceCollector is stopped`() { + fun `when transaction is finished, compositePerformanceCollector is stopped`() { val transaction = fixture.getSut() transaction.finish() - verify(fixture.transactionPerformanceCollector).stop(check { assertEquals(transaction, it) }) + verify(fixture.compositePerformanceCollector).stop(check { assertEquals(transaction, it) }) } @Test - fun `when a span is started and finished the transactionPerformanceCollector gets notified`() { + fun `when a span is started and finished the compositePerformanceCollector gets notified`() { val transaction = fixture.getSut() val span = transaction.startChild("op.span") span.finish() - verify(fixture.transactionPerformanceCollector).onSpanStarted(check { assertEquals(span, it) }) - verify(fixture.transactionPerformanceCollector).onSpanFinished(check { assertEquals(span, it) }) + verify(fixture.compositePerformanceCollector).onSpanStarted(check { assertEquals(span, it) }) + verify(fixture.compositePerformanceCollector).onSpanFinished(check { assertEquals(span, it) }) } @Test @@ -1208,11 +1271,13 @@ class SentryTracerTest { @Test fun `when transaction is finished, collected performance data is cleared`() { val data = mutableListOf(mock(), mock()) - val mockPerformanceCollector = object : TransactionPerformanceCollector { + val mockPerformanceCollector = object : CompositePerformanceCollector { override fun start(transaction: ITransaction) {} + override fun start(id: String) {} override fun onSpanStarted(span: ISpan) {} override fun onSpanFinished(span: ISpan) {} override fun stop(transaction: ITransaction): MutableList = data + override fun stop(id: String): MutableList = data override fun close() {} } val transaction = fixture.getSut(optionsConfiguration = { @@ -1363,6 +1428,7 @@ class SentryTracerTest { fun `when a span is launched on the main thread, the thread info should be set correctly`() { val threadChecker = mock() whenever(threadChecker.isMainThread).thenReturn(true) + whenever(threadChecker.currentThreadName).thenReturn("main") val tracer = fixture.getSut(optionsConfiguration = { options -> options.threadChecker = threadChecker @@ -1376,6 +1442,7 @@ class SentryTracerTest { fun `when a span is launched on the background thread, the thread info should be set correctly`() { val threadChecker = mock() whenever(threadChecker.isMainThread).thenReturn(false) + whenever(threadChecker.currentThreadName).thenReturn("test") val tracer = fixture.getSut(optionsConfiguration = { options -> options.threadChecker = threadChecker diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index 5e7ba9de25..47b98d5ee8 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -13,6 +13,13 @@ class SpanContextTest { assertNotNull(trace.spanId) } + @Test + fun `when created with default constructor, generates thread id and name`() { + val trace = SpanContext("op") + assertNotNull(trace.data[SpanDataConvention.THREAD_ID]) + assertNotNull(trace.data[SpanDataConvention.THREAD_NAME]) + } + @Test fun `sets tag`() { val trace = SpanContext("op") diff --git a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt index 707daa78f1..2ebc830a5e 100644 --- a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt @@ -6,6 +6,7 @@ import io.sentry.JsonObjectReader import io.sentry.JsonObjectWriter import io.sentry.JsonSerializable import io.sentry.SpanContext +import io.sentry.SpanDataConvention import io.sentry.SpanId import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision @@ -35,6 +36,8 @@ class SpanContextSerializationTest { setTag("2a5fa3f5-7b87-487f-aaa5-84567aa73642", "4781d51a-c5af-47f2-a4ed-f030c9b3e194") setTag("29106d7d-7fa4-444f-9d34-b9d7510c69ab", "218c23ea-694a-497e-bf6d-e5f26f1ad7bd") setTag("ba9ce913-269f-4c03-882d-8ca5e6991b14", "35a74e90-8db8-4610-a411-872cbc1030ac") + data[SpanDataConvention.THREAD_NAME] = "test" + data[SpanDataConvention.THREAD_ID] = 10 setData("spanContextDataKey", "spanContextDataValue") } } diff --git a/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt index 26de021fbd..12b1e34827 100644 --- a/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt +++ b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt @@ -2,6 +2,7 @@ package io.sentry.util.thread import io.sentry.protocol.SentryThread import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -43,4 +44,11 @@ class ThreadCheckerTest { } assertFalse(threadChecker.isMainThread(sentryThread)) } + + @Test + fun `currentThreadName returns the name of the current thread`() { + val thread = Thread.currentThread() + thread.name = "test" + assertEquals("test", threadChecker.currentThreadName) + } } diff --git a/sentry/src/test/resources/json/checkin_crontab.json b/sentry/src/test/resources/json/checkin_crontab.json index 8c39685878..c2bff2a050 100644 --- a/sentry/src/test/resources/json/checkin_crontab.json +++ b/sentry/src/test/resources/json/checkin_crontab.json @@ -25,7 +25,12 @@ "trace_id": "f382e3180c714217a81371f8c644aefe", "span_id": "85694b9f567145a6", "op": "default", - "origin": "manual" + "origin": "manual", + "data": + { + "thread.name": "test", + "thread.id": 10 + } } } } diff --git a/sentry/src/test/resources/json/checkin_interval.json b/sentry/src/test/resources/json/checkin_interval.json index 8281ca67ab..395bb03bba 100644 --- a/sentry/src/test/resources/json/checkin_interval.json +++ b/sentry/src/test/resources/json/checkin_interval.json @@ -26,7 +26,12 @@ "trace_id": "f382e3180c714217a81371f8c644aefe", "span_id": "85694b9f567145a6", "op": "default", - "origin": "manual" + "origin": "manual", + "data": + { + "thread.name": "test", + "thread.id": 10 + } } } } diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index a6f35b31a6..e8df6a21c1 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -123,7 +123,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } } diff --git a/sentry/src/test/resources/json/sentry_base_event.json b/sentry/src/test/resources/json/sentry_base_event.json index d2d1fd0088..63ae8f03cf 100644 --- a/sentry/src/test/resources/json/sentry_base_event.json +++ b/sentry/src/test/resources/json/sentry_base_event.json @@ -126,7 +126,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json index 4ce74eaf09..2079b424cb 100644 --- a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json +++ b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json @@ -126,7 +126,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index 6d421fc993..c6f8dd68b0 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -261,7 +261,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_replay_event.json b/sentry/src/test/resources/json/sentry_replay_event.json index d3970bf5b0..7bd64037d7 100644 --- a/sentry/src/test/resources/json/sentry_replay_event.json +++ b/sentry/src/test/resources/json/sentry_replay_event.json @@ -144,7 +144,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction.json b/sentry/src/test/resources/json/sentry_transaction.json index 33080c9686..daa6d025e9 100644 --- a/sentry/src/test/resources/json/sentry_transaction.json +++ b/sentry/src/test/resources/json/sentry_transaction.json @@ -183,7 +183,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json index 0d6ed5eb09..316b44bbaa 100644 --- a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json +++ b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json @@ -183,7 +183,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json index 2d965be1b9..cf927b322b 100644 --- a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json +++ b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json @@ -153,7 +153,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/span_context.json b/sentry/src/test/resources/json/span_context.json index c55841a391..edff574fa4 100644 --- a/sentry/src/test/resources/json/span_context.json +++ b/sentry/src/test/resources/json/span_context.json @@ -14,6 +14,8 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } }