From f0d8942d3df55a79e4c3ea3e0a0c5691b4720f81 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 9 Sep 2024 18:07:53 +0200 Subject: [PATCH 01/14] added IContinuousProfiler and implementations made AndroidProfiler's executor nullable, as timeout will be handled differently for continuous profiling --- .../api/sentry-android-core.api | 9 + .../core/AndroidContinuousProfiler.java | 170 ++++++++++ .../sentry/android/core/AndroidProfiler.java | 14 +- .../core/AndroidTransactionProfiler.java | 1 + .../core/AndroidContinuousProfilerTest.kt | 319 ++++++++++++++++++ sentry/api/sentry.api | 17 + .../java/io/sentry/IContinuousProfiler.java | 19 ++ .../io/sentry/NoOpContinuousProfiler.java | 31 ++ .../io/sentry/NoOpContinuousProfilerTest.kt | 25 ++ 9 files changed, 600 insertions(+), 5 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt create mode 100644 sentry/src/main/java/io/sentry/IContinuousProfiler.java create mode 100644 sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java create mode 100644 sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index f1d5f8e7d7..62cd3bfcf0 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -36,6 +36,15 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } +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 close ()V + public fun isRunning ()Z + public fun setHub (Lio/sentry/IHub;)V + public fun start ()V + public fun stop ()V +} + public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector { public fun (Lio/sentry/ILogger;Lio/sentry/android/core/BuildInfoProvider;)V public fun collect (Lio/sentry/PerformanceCollectionData;)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 new file mode 100644 index 0000000000..8c980be7a2 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -0,0 +1,170 @@ +package io.sentry.android.core; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.annotation.SuppressLint; +import android.os.Build; +import io.sentry.IContinuousProfiler; +import io.sentry.IHub; +import io.sentry.ILogger; +import io.sentry.ISentryExecutorService; +import io.sentry.SentryLevel; +import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import java.util.concurrent.Future; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +@ApiStatus.Internal +public class AndroidContinuousProfiler implements IContinuousProfiler { + private static final long MAX_CHUNK_DURATION_MILLIS = 10000; + + 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; + private boolean isInitialized = false; + private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; + private @Nullable AndroidProfiler profiler = null; + private boolean isRunning = false; + private @Nullable IHub hub; + private @Nullable Future closeFuture; + + public AndroidContinuousProfiler( + final @NotNull BuildInfoProvider buildInfoProvider, + 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; + } + + private void init() { + // We initialize it only once + if (isInitialized) { + return; + } + isInitialized = true; + if (!isProfilingEnabled) { + logger.log(SentryLevel.INFO, "Profiling is disabled in options."); + return; + } + if (profilingTracesDirPath == null) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); + return; + } + if (profilingTracesHz <= 0) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + profilingTracesHz); + return; + } + + profiler = + new AndroidProfiler( + profilingTracesDirPath, + (int) SECONDS.toMicros(1) / profilingTracesHz, + frameMetricsCollector, + null, + logger, + buildInfoProvider); + } + + public synchronized void setHub(final @NotNull IHub hub) { + this.hub = hub; + } + + public synchronized void start() { + // 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; + + // Let's initialize trace folder and profiling interval + init(); + // init() didn't create profiler, should never happen + if (profiler == null) { + return; + } + + final AndroidProfiler.ProfileStartData startData = profiler.start(); + // check if profiling started + if (startData == null) { + return; + } + isRunning = true; + + closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + } + + public synchronized void stop() { + stop(false); + } + + @SuppressLint("NewApi") + private synchronized void stop(final boolean restartProfiler) { + if (closeFuture != null) { + closeFuture.cancel(true); + } + // check if profiler was created and it's running + if (profiler == null || !isRunning) { + return; + } + + // onTransactionStart() is only available since Lollipop_MR1 + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) { + return; + } + + // todo add PerformanceCollectionData + final AndroidProfiler.ProfileEndData endData = profiler.endAndCollect(false, null); + + // check if profiler end successfully + if (endData == null) { + return; + } + + isRunning = false; + + // todo schedule capture profile chunk envelope + + if (restartProfiler) { + logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); + start(); + } else { + logger.log(SentryLevel.DEBUG, "Profile chunk finished."); + } + } + + public synchronized void close() { + if (closeFuture != null) { + closeFuture.cancel(true); + } + stop(); + } + + @Override + public boolean isRunning() { + return isRunning; + } + + @VisibleForTesting + @Nullable + Future getCloseFuture() { + return closeFuture; + } +} 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 d24025c551..3d2e8a9205 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 @@ -93,7 +93,7 @@ public ProfileEndData( new ArrayDeque<>(); private final @NotNull Map measurementsMap = new HashMap<>(); private final @NotNull BuildInfoProvider buildInfoProvider; - private final @NotNull ISentryExecutorService executorService; + private final @Nullable ISentryExecutorService timeoutExecutorService; private final @NotNull ILogger logger; private boolean isRunning = false; @@ -101,14 +101,15 @@ public AndroidProfiler( final @NotNull String tracesFilesDirPath, final int intervalUs, final @NotNull SentryFrameMetricsCollector frameMetricsCollector, - final @NotNull ISentryExecutorService executorService, + final @Nullable ISentryExecutorService timeoutExecutorService, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { this.traceFilesDir = new File(Objects.requireNonNull(tracesFilesDirPath, "TracesFilesDirPath is required")); this.intervalUs = intervalUs; this.logger = Objects.requireNonNull(logger, "Logger is required"); - this.executorService = Objects.requireNonNull(executorService, "ExecutorService is required."); + // Timeout executor is nullable, as timeouts will not be there for continuous profiling + this.timeoutExecutorService = timeoutExecutorService; this.frameMetricsCollector = Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required"); this.buildInfoProvider = @@ -185,8 +186,11 @@ public void onFrameMetricCollected( // We stop profiling after a timeout to avoid huge profiles to be sent try { - scheduledFinish = - executorService.schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + if (timeoutExecutorService != null) { + scheduledFinish = + timeoutExecutorService.schedule( + () -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + } } catch (RejectedExecutionException e) { logger.log( SentryLevel.ERROR, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index eca5d744f6..6ae79f0996 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -212,6 +212,7 @@ public synchronized void bindTransaction(final @NotNull ITransaction transaction // onTransactionStart() is only available since Lollipop_MR1 // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + // and SUPPORTED_ABIS since KITKAT if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; // Transaction finished, but it's not in the current profile 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 new file mode 100644 index 0000000000..3efc1f0cf5 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -0,0 +1,319 @@ +package io.sentry.android.core + +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.ISentryExecutorService +import io.sentry.SentryLevel +import io.sentry.SentryTracer +import io.sentry.TransactionContext +import io.sentry.android.core.internal.util.SentryFrameMetricsCollector +import io.sentry.test.DeferredExecutorService +import io.sentry.test.getProperty +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.io.File +import java.util.concurrent.Callable +import java.util.concurrent.Future +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class AndroidContinuousProfilerTest { + private lateinit var context: Context + private val fixture = Fixture() + + private class Fixture { + private val mockDsn = "http://key@localhost/proj" + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) + } + val mockLogger = mock() + + val hub: IHub = mock() + val frameMetricsCollector: SentryFrameMetricsCollector = mock() + + lateinit var transaction1: SentryTracer + lateinit var transaction2: SentryTracer + lateinit var transaction3: SentryTracer + + val options = spy(SentryAndroidOptions()).apply { + dsn = mockDsn + profilesSampleRate = 1.0 + isDebug = true + setLogger(mockLogger) + } + + 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) + return AndroidContinuousProfiler( + buildInfoProvider, + frameMetricsCollector, + options.logger, + options.profilingTracesDirPath, + options.isProfilingEnabled, + options.profilingTracesHz, + options.executorService + ) + } + } + + @BeforeTest + fun `set up`() { + context = ApplicationProvider.getApplicationContext() + val buildInfoProvider = BuildInfoProvider(fixture.mockLogger) + val loadClass = LoadClass() + val activityFramesTracker = ActivityFramesTracker(loadClass, fixture.options) + AndroidOptionsInitializer.loadDefaultAndMetadataOptions( + fixture.options, + context, + fixture.mockLogger, + buildInfoProvider + ) + + AndroidOptionsInitializer.installDefaultIntegrations( + context, + fixture.options, + buildInfoProvider, + loadClass, + activityFramesTracker, + false, + false, + false + ) + + AndroidOptionsInitializer.initializeIntegrationsAndProcessors( + fixture.options, + context, + buildInfoProvider, + loadClass, + activityFramesTracker + ) + // 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() + } + + @AfterTest + fun clear() { + context.cacheDir.deleteRecursively() + } + + @Test + fun `isRunning reflects profiler status`() { + val profiler = fixture.getSut() + profiler.start() + assertTrue(profiler.isRunning) + profiler.stop() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler multiple starts are ignored`() { + val profiler = fixture.getSut() + profiler.start() + assertTrue(profiler.isRunning) + verify(fixture.mockLogger, never()).log(eq(SentryLevel.WARNING), eq("Profiling has already started...")) + profiler.start() + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Profiling has already started...")) + assertTrue(profiler.isRunning) + } + + @Test + fun `profiler works only on api 22+`() { + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) + } + val profiler = fixture.getSut(buildInfo) + profiler.start() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on profilesSampleRate=0 false`() { + 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.") + } + + @Test + fun `profiler evaluates profilingTracesDirPath options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { + it.cacheDirPath = null + } + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined 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.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options." + ) + } + + @Test + fun `profiler evaluates profilingTracesHz options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { + it.profilingTracesHz = 0 + } + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + 0 + ) + + // 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.WARNING, + "Disabling profiling because trace rate is set to %d", + 0 + ) + } + + @Test + fun `profiler on tracesDirPath null`() { + val profiler = fixture.getSut { + it.cacheDirPath = null + } + profiler.start() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on tracesDirPath empty`() { + val profiler = fixture.getSut { + it.cacheDirPath = "" + } + profiler.start() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on profilingTracesHz 0`() { + val profiler = fixture.getSut { + it.profilingTracesHz = 0 + } + profiler.start() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler never use background threads`() { + val mockExecutorService: ISentryExecutorService = mock() + val profiler = fixture.getSut { + it.executorService = mockExecutorService + } + whenever(mockExecutorService.submit(any>())).thenReturn(mock()) + profiler.start() + verify(mockExecutorService, never()).submit(any()) + profiler.stop() + verify(mockExecutorService, never()).submit(any>()) + } + + @Test + fun `profiler does not throw if traces cannot be written to disk`() { + val profiler = fixture.getSut { + File(it.profilingTracesDirPath!!).setWritable(false) + } + profiler.start() + profiler.stop() + // We assert that no trace files are written + assertTrue( + File(fixture.options.profilingTracesDirPath!!) + .list()!! + .isEmpty() + ) + verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Error while stopping profiling: "), any()) + } + + @Test + fun `profiler stops collecting frame metrics when it stops`() { + val profiler = fixture.getSut() + val frameMetricsCollectorId = "id" + whenever(fixture.frameMetricsCollector.startCollection(any())).thenReturn(frameMetricsCollectorId) + profiler.start() + verify(fixture.frameMetricsCollector, never()).stopCollection(frameMetricsCollectorId) + profiler.stop() + verify(fixture.frameMetricsCollector).stopCollection(frameMetricsCollectorId) + } + + @Test + fun `profiler stops profiling and clear scheduled job on close`() { + val profiler = fixture.getSut() + profiler.start() + assertTrue(profiler.isRunning) + + profiler.close() + assertFalse(profiler.isRunning) + + // The timeout scheduled job should be cleared + val androidProfiler = profiler.getProperty("profiler") + val scheduledJob = androidProfiler?.getProperty?>("scheduledFinish") + assertNull(scheduledJob) + + val closeFuture = profiler.closeFuture + assertNotNull(closeFuture) + assertTrue(closeFuture.isCancelled) + } + + @Test + fun `profiler stops and restart for each chunk`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + + executorService.runAll() + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) + assertTrue(profiler.isRunning) + + executorService.runAll() + verify(fixture.mockLogger, times(2)).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) + assertTrue(profiler.isRunning) + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4129162237..f3e79a2966 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -697,6 +697,14 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V } +public abstract interface class io/sentry/IContinuousProfiler { + public abstract fun close ()V + public abstract fun isRunning ()Z + public abstract fun setHub (Lio/sentry/IHub;)V + public abstract fun start ()V + public abstract fun stop ()V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -1384,6 +1392,15 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V } +public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfiler { + public fun close ()V + public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; + public fun isRunning ()Z + public fun setHub (Lio/sentry/IHub;)V + public fun start ()V + public fun stop ()V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java new file mode 100644 index 0000000000..ac26566d78 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -0,0 +1,19 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** Used for performing operations when a transaction is started or ended. */ +@ApiStatus.Internal +public interface IContinuousProfiler { + boolean isRunning(); + + void start(); + + void stop(); + + void setHub(final @NotNull IHub hub); + + /** Cancel the profiler and stops it. Used on SDK close. */ + void close(); +} diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java new file mode 100644 index 0000000000..ea45bef74c --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -0,0 +1,31 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; + +public final class NoOpContinuousProfiler implements IContinuousProfiler { + + private static final NoOpContinuousProfiler instance = new NoOpContinuousProfiler(); + + private NoOpContinuousProfiler() {} + + public static NoOpContinuousProfiler getInstance() { + return instance; + } + + @Override + public void start() {} + + @Override + public void stop() {} + + @Override + public void setHub(@NotNull IHub hub) {} + + @Override + public boolean isRunning() { + return false; + } + + @Override + public void close() {} +} diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt new file mode 100644 index 0000000000..afbce4a8cb --- /dev/null +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -0,0 +1,25 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertFalse + +class NoOpContinuousProfilerTest { + private var profiler = NoOpContinuousProfiler.getInstance() + + @Test + fun `start does not throw`() = + profiler.start() + + @Test + fun `stop does not throw`() = + profiler.stop() + + @Test + fun `isRunning returns false`() { + assertFalse(profiler.isRunning) + } + + @Test + fun `close does not throw`() = + profiler.close() +} From ec061f9f18e8a5524c94d466dcc899e8084addb0 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 17 Sep 2024 15:39:20 +0200 Subject: [PATCH 02/14] updated IHub to IScopes --- sentry-android-core/api/sentry-android-core.api | 2 +- .../io/sentry/android/core/AndroidContinuousProfiler.java | 8 ++++---- sentry/api/sentry.api | 4 ++-- sentry/src/main/java/io/sentry/IContinuousProfiler.java | 2 +- .../src/main/java/io/sentry/NoOpContinuousProfiler.java | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 62cd3bfcf0..23c798b561 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -40,7 +40,7 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti 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 close ()V public fun isRunning ()Z - public fun setHub (Lio/sentry/IHub;)V + public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()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 8c980be7a2..f8155b38b9 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 @@ -5,8 +5,8 @@ import android.annotation.SuppressLint; import android.os.Build; import io.sentry.IContinuousProfiler; -import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScopes; import io.sentry.ISentryExecutorService; import io.sentry.SentryLevel; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; @@ -30,7 +30,7 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; private @Nullable AndroidProfiler profiler = null; private boolean isRunning = false; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable Future closeFuture; public AndroidContinuousProfiler( @@ -84,8 +84,8 @@ private void init() { buildInfoProvider); } - public synchronized void setHub(final @NotNull IHub hub) { - this.hub = hub; + public synchronized void setScopes(final @NotNull IScopes scopes) { + this.scopes = scopes; } public synchronized void start() { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f3e79a2966..577b94a6d3 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -700,7 +700,7 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract interface class io/sentry/IContinuousProfiler { public abstract fun close ()V public abstract fun isRunning ()Z - public abstract fun setHub (Lio/sentry/IHub;)V + public abstract fun setScopes (Lio/sentry/IScopes;)V public abstract fun start ()V public abstract fun stop ()V } @@ -1396,7 +1396,7 @@ public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfi public fun close ()V public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; public fun isRunning ()Z - public fun setHub (Lio/sentry/IHub;)V + public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()V } diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java index ac26566d78..c94eb9bba3 100644 --- a/sentry/src/main/java/io/sentry/IContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -12,7 +12,7 @@ public interface IContinuousProfiler { void stop(); - void setHub(final @NotNull IHub hub); + void setScopes(final @NotNull IScopes scopes); /** Cancel the profiler and stops it. Used on SDK close. */ void close(); diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java index ea45bef74c..b17123029f 100644 --- a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -19,7 +19,7 @@ public void start() {} public void stop() {} @Override - public void setHub(@NotNull IHub hub) {} + public void setScopes(@NotNull IScopes scopes) {} @Override public boolean isRunning() { From 209b03d865337e8e0b61882d09e2149ad9b7b1f0 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 11 Nov 2024 15:22:01 +0100 Subject: [PATCH 03/14] wrapped continuous profiler close future in a try catch block --- .../core/AndroidContinuousProfiler.java | 13 ++++++---- .../sentry/android/core/AndroidProfiler.java | 24 +++++++++---------- 2 files changed, 21 insertions(+), 16 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 f8155b38b9..037b110c3d 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 @@ -11,6 +11,7 @@ import io.sentry.SentryLevel; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -107,7 +108,14 @@ public synchronized void start() { } isRunning = true; - closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + 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); + } } public synchronized void stop() { @@ -151,9 +159,6 @@ private synchronized void stop(final boolean restartProfiler) { } public synchronized void close() { - if (closeFuture != null) { - closeFuture.cancel(true); - } stop(); } 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 155dc31b14..8d4e37a40c 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 @@ -188,19 +188,19 @@ public void onFrameMetricCollected( } }); - // We stop profiling after a timeout to avoid huge profiles to be sent - try { - if (timeoutExecutorService != null) { - scheduledFinish = - timeoutExecutorService.schedule( - () -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + // We stop profiling after a timeout to avoid huge profiles to be sent + try { + if (timeoutExecutorService != null) { + scheduledFinish = + timeoutExecutorService.schedule( + () -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + } + } catch (RejectedExecutionException e) { + logger.log( + SentryLevel.ERROR, + "Failed to call the executor. Profiling will not be automatically finished. Did you call Sentry.close()?", + e); } - } catch (RejectedExecutionException e) { - logger.log( - SentryLevel.ERROR, - "Failed to call the executor. Profiling will not be automatically finished. Did you call Sentry.close()?", - e); - } profileStartNanos = SystemClock.elapsedRealtimeNanos(); final @NotNull Date profileStartTimestamp = DateUtils.getCurrentDateTime(); From a9235ae1962c5061f3bd18a20e9e32da9a2c1b4d Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 14 Nov 2024 15:57:37 +0100 Subject: [PATCH 04/14] Change payload for Continuous Profiling v8 (p2) (#3711) * 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 * removed timestampMillis from MemoryCollectionData and CpuCollectionData, now it uses timestamp.nanotime() to achieve same result * continuous profiler doesn't stop anymore when an error occurs, but continue scheduling restart 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 | 116 +++++-- .../android/core/AndroidCpuCollector.java | 3 +- .../android/core/AndroidMemoryCollector.java | 5 +- .../core/AndroidOptionsInitializer.java | 87 ++++- .../sentry/android/core/AndroidProfiler.java | 27 +- .../core/SentryPerformanceProvider.java | 93 +++-- .../internal/util/AndroidThreadChecker.java | 5 + .../core/performance/AppStartMetrics.java | 21 +- .../core/AndroidContinuousProfilerTest.kt | 130 +++++-- .../android/core/AndroidCpuCollectorTest.kt | 2 +- .../core/AndroidMemoryCollectorTest.kt | 2 +- .../core/AndroidOptionsInitializerTest.kt | 116 ++++++- .../android/core/AndroidProfilerTest.kt | 9 +- .../core/AndroidTransactionProfilerTest.kt | 6 +- .../core/SentryPerformanceProviderTest.kt | 65 +++- .../core/SessionTrackingIntegrationTest.kt | 5 + .../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 | 198 ++++++++--- ...ava => CompositePerformanceCollector.java} | 11 +- .../java/io/sentry/CpuCollectionData.java | 11 +- ...DefaultCompositePerformanceCollector.java} | 36 +- .../java/io/sentry/DefaultSpanFactory.java | 4 +- .../src/main/java/io/sentry/HubAdapter.java | 16 + .../main/java/io/sentry/HubScopesWrapper.java | 15 + .../java/io/sentry/IContinuousProfiler.java | 6 +- sentry/src/main/java/io/sentry/IScopes.java | 14 + .../main/java/io/sentry/ISentryClient.java | 11 + .../src/main/java/io/sentry/ISpanFactory.java | 2 +- .../java/io/sentry/JavaMemoryCollector.java | 4 +- .../main/java/io/sentry/JsonSerializer.java | 2 + .../java/io/sentry/MainEventProcessor.java | 31 +- .../java/io/sentry/MemoryCollectionData.java | 15 +- ...=> NoOpCompositePerformanceCollector.java} | 18 +- .../io/sentry/NoOpContinuousProfiler.java | 9 +- sentry/src/main/java/io/sentry/NoOpHub.java | 11 + .../src/main/java/io/sentry/NoOpScopes.java | 11 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../main/java/io/sentry/NoOpSpanFactory.java | 2 +- .../src/main/java/io/sentry/ProfileChunk.java | 317 ++++++++++++++++++ .../main/java/io/sentry/ProfileContext.java | 120 +++++++ sentry/src/main/java/io/sentry/Scopes.java | 68 +++- .../main/java/io/sentry/ScopesAdapter.java | 15 + sentry/src/main/java/io/sentry/Sentry.java | 10 + .../SentryAppStartProfilingOptions.java | 21 ++ .../src/main/java/io/sentry/SentryClient.java | 40 ++- .../java/io/sentry/SentryEnvelopeItem.java | 62 +++- .../main/java/io/sentry/SentryItemType.java | 1 + .../main/java/io/sentry/SentryOptions.java | 58 +++- .../src/main/java/io/sentry/SentryTracer.java | 46 +-- .../src/main/java/io/sentry/SpanContext.java | 17 + .../java/io/sentry/SpanDataConvention.java | 1 + .../ProfileMeasurementValue.java | 43 ++- .../java/io/sentry/protocol/Contexts.java | 15 + .../java/io/sentry/protocol/DebugMeta.java | 38 +++ .../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 | 16 + .../java/io/sentry/JavaMemoryCollectorTest.kt | 2 +- .../test/java/io/sentry/JsonSerializerTest.kt | 273 +++++++++++++-- .../io/sentry/NoOpContinuousProfilerTest.kt | 7 + sentry/src/test/java/io/sentry/NoOpHubTest.kt | 10 + .../java/io/sentry/NoOpSentryClientTest.kt | 4 + .../test/java/io/sentry/OutboxSenderTest.kt | 1 + .../sentry/PerformanceCollectionDataTest.kt | 15 +- .../test/java/io/sentry/ScopesAdapterTest.kt | 16 + sentry/src/test/java/io/sentry/ScopesTest.kt | 103 +++++- .../test/java/io/sentry/SentryClientTest.kt | 57 +++- .../java/io/sentry/SentryEnvelopeItemTest.kt | 88 +++++ .../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 + .../java/io/sentry/protocol/ContextsTest.kt | 6 + .../java/io/sentry/protocol/DebugMetaTest.kt | 72 ++++ .../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 +- 100 files changed, 2722 insertions(+), 357 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%) create mode 100644 sentry/src/main/java/io/sentry/ProfileChunk.java create mode 100644 sentry/src/main/java/io/sentry/ProfileContext.java 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 037b110c3d..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,12 +4,21 @@ 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; +import io.sentry.protocol.SentryId; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.ApiStatus; @@ -23,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; @@ -32,21 +40,23 @@ 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; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, 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; } @@ -57,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, @@ -81,15 +87,17 @@ private void init() { (int) SECONDS.toMicros(1) / profilingTracesHz, frameMetricsCollector, null, - logger, - buildInfoProvider); - } - - public synchronized void setScopes(final @NotNull IScopes scopes) { - this.scopes = scopes; + logger); } 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; @@ -106,10 +114,23 @@ 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(); + } + + 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, @@ -124,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) { @@ -138,22 +159,44 @@ 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) { - return; + 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; + // 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."); } } @@ -162,6 +205,33 @@ 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 + .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; @@ -169,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/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java index ccd878761d..800d4826fe 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 @@ -8,6 +8,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; @@ -73,7 +74,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti CpuCollectionData cpuData = new CpuCollectionData( - System.currentTimeMillis(), (cpuUsagePercentage / (double) numCores) * 100.0); + (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..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 @@ -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; @@ -15,10 +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); + MemoryCollectionData memoryData = + new MemoryCollectionData(usedMemory, usedNativeMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } 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/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index 0b31c8a206..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 @@ -11,7 +11,9 @@ import io.sentry.ISentryLifecycleToken; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryDate; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.SentryUUID; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; @@ -154,6 +156,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() @@ -167,15 +170,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)); } } }); @@ -318,20 +324,23 @@ private void putPerformanceCollectionDataInMeasurements( if (cpuData != null) { cpuUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff, - cpuData.getCpuUsagePercentage())); + 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.getUsedHeapMemory())); + 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.getUsedNativeMemory())); + memoryData.getTimestamp().nanoTimestamp() + timestampDiff, + memoryData.getUsedNativeMemory(), + memoryData.getTimestamp())); } } } 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 3efc1f0cf5..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.IHub +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 @@ -45,7 +54,7 @@ class AndroidContinuousProfilerTest { } val mockLogger = mock() - val hub: IHub = mock() + val scopes: IScopes = mock() val frameMetricsCollector: SentryFrameMetricsCollector = mock() lateinit var transaction1: SentryTracer @@ -61,16 +70,15 @@ 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, options.logger, options.profilingTracesDirPath, - options.isProfilingEnabled, options.profilingTracesHz, options.executorService ) @@ -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`() { - 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 + fun `profiler ignores profilesSampleRate`() { 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 @@ -316,4 +333,65 @@ 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 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() + 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 7f54b2c353..f86415037a 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 @@ -45,6 +45,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 7879c2daf5..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,6 +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/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/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index 670729c4d7..86e0d90001 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 @@ -8,6 +8,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 @@ -258,12 +259,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(2, 3, t1)) + singleData.addCpuData(CpuCollectionData(1.4, t1)) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + 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 b92db7cfc6..ec9e737559 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 @@ -460,12 +460,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(2, 3, mock())) + singleData.addCpuData(CpuCollectionData(1.4, mock())) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(3, 4, mock())) performanceCollectionData.add(singleData) profiler.start() 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/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index e392d238ab..eac9f4293f 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 @@ -10,6 +10,7 @@ import io.sentry.CheckIn import io.sentry.Hint import io.sentry.IScope import io.sentry.ISentryClient +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.Sentry import io.sentry.SentryEnvelope @@ -176,6 +177,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-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 e0eefc49bf..f3a84217b5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -311,10 +311,20 @@ 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 (JD)V + public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D - public fun getTimestampMillis ()J + public fun getTimestamp ()Lio/sentry/SentryDate; } public final class io/sentry/CustomSamplingContext { @@ -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 { @@ -560,6 +572,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 @@ -606,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 } @@ -625,6 +640,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 @@ -670,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 } @@ -698,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 } @@ -856,6 +874,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; @@ -905,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 } @@ -934,6 +955,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 @@ -1014,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 { @@ -1248,9 +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 getTimestampMillis ()J + public fun (JJLio/sentry/SentryDate;)V + public fun (JLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J } @@ -1354,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 @@ -1365,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 } @@ -1388,6 +1421,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 @@ -1435,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 } @@ -1547,6 +1583,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 @@ -1594,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 } @@ -1655,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; } @@ -1712,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 @@ -1806,6 +1836,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 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 + 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; @@ -2137,6 +2239,7 @@ public final class io/sentry/Scopes : 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 @@ -2183,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 } @@ -2201,6 +2306,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 @@ -2247,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 } @@ -2351,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 } @@ -2372,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 @@ -2393,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; @@ -2485,6 +2598,7 @@ 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 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; @@ -2564,6 +2678,7 @@ 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 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; @@ -2695,6 +2810,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field Event Lio/sentry/SentryItemType; public static final field Feedback 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; @@ -2816,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; @@ -2885,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; @@ -2893,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 @@ -2929,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 @@ -3009,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 @@ -3496,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; } @@ -3679,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; @@ -4244,9 +4356,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 @@ -4262,6 +4375,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 } @@ -4359,6 +4473,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 @@ -4377,6 +4492,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 @@ -4439,6 +4555,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; @@ -6253,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 @@ -6262,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 @@ -6271,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/CpuCollectionData.java b/sentry/src/main/java/io/sentry/CpuCollectionData.java index 081063a53f..bcbab7c136 100644 --- a/sentry/src/main/java/io/sentry/CpuCollectionData.java +++ b/sentry/src/main/java/io/sentry/CpuCollectionData.java @@ -1,19 +1,20 @@ 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) { - this.timestampMillis = timestampMillis; + public CpuCollectionData(final double cpuUsagePercentage, final @NotNull SentryDate timestamp) { this.cpuUsagePercentage = cpuUsagePercentage; + this.timestamp = timestamp; } - public long getTimestampMillis() { - return timestampMillis; + public @NotNull SentryDate getTimestamp() { + return timestamp; } public double getCpuUsagePercentage() { 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 fc2f9c15dc..537bcdf104 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -277,6 +277,22 @@ 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) { + return Sentry.getCurrentScopes().captureProfileChunk(profilingContinuousData); + } + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 591852a9ad..d6755c2c41 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -265,6 +265,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, @@ -272,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 e07de9c327..59a577f04b 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -527,6 +527,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. * @@ -582,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/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 017b283d57..22389f2b6e 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/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/JavaMemoryCollector.java b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java index cdde808ba5..9ede59ba07 100644 --- a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java +++ b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java @@ -13,9 +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); + MemoryCollectionData memoryData = + new MemoryCollectionData(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 09773dc617..bb2d356a1d 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -89,6 +89,8 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(Mechanism.class, new Mechanism.Deserializer()); deserializersByClass.put(Message.class, new Message.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 a5cbacc4df..d76f25c562 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; @@ -68,34 +67,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..1155e00b4b 100644 --- a/sentry/src/main/java/io/sentry/MemoryCollectionData.java +++ b/sentry/src/main/java/io/sentry/MemoryCollectionData.java @@ -1,26 +1,27 @@ 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) { - 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) { - this(timestampMillis, usedHeapMemory, -1); + public MemoryCollectionData(final long usedHeapMemory, final @NotNull SentryDate timestamp) { + this(usedHeapMemory, -1, timestamp); } - public long getTimestampMillis() { - return timestampMillis; + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getUsedHeapMemory() { 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 d3e0b010c3..925f1e64ee 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -231,6 +231,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, @@ -238,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 8255569387..11bae042b0 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -226,6 +226,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, @@ -233,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/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 162b1fae5a..4bee000805 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -58,6 +58,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/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 new file mode 100644 index 0000000000..725c151dbd --- /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 = new ConcurrentHashMap<>(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..e4b411c279 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileContext.java @@ -0,0 +1,120 @@ +package io.sentry; + +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; + +public final 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 67b38f48bc..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( @@ -809,6 +810,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, @@ -868,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 @@ -893,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 6df6deee3d..9944a87a0a 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -268,6 +268,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, @@ -275,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/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index fdeb8b9b6b..e726e5d750 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -7,6 +7,7 @@ import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; 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; @@ -463,8 +464,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) { @@ -853,6 +853,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 2d5f6484b3..62892e3ed4 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -34,6 +34,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"); @@ -255,13 +258,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( @@ -274,8 +328,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 85acc0aadd..068c37ab1f 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/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 91e3abd956..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()); } /** @@ -110,10 +116,21 @@ 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; + } + 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/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 53c97bcb0b..e9fd594bfd 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -7,6 +7,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.AutoClosableReentrantLock; import io.sentry.util.HintUtils; @@ -54,6 +55,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 { @@ -77,6 +80,15 @@ public void setTrace(final @NotNull 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); } @@ -263,6 +275,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/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 f57c45ba4e..310c153709 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() @@ -259,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/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index e2914edff6..7e076bba78 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -23,6 +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/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index e9d12ff4b1..53dd7d85bf 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 @@ -500,10 +503,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" @@ -533,22 +555,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)) ) ) ) @@ -604,7 +626,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 ) ) ), @@ -614,7 +637,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 ) ) ), @@ -624,7 +648,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 ) ) ), @@ -634,7 +659,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 ) ) ) @@ -765,23 +791,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) @@ -798,10 +824,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) } @@ -810,22 +837,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) } @@ -833,10 +860,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 @@ -846,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") @@ -879,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) @@ -941,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")) @@ -1004,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) } @@ -1021,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) @@ -1324,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 f20257482d..e0eb08ded0 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -31,6 +31,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())) @@ -111,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/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/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/PerformanceCollectionDataTest.kt b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt index e105e105c6..76866b09e6 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, t1) + val memData2 = MemoryCollectionData(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, t1) + val cpuData2 = CpuCollectionData(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, 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 db2ed593ca..9c7418c6b5 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() @@ -259,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 a806ca2175..68a1fee5d6 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1576,6 +1576,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 @@ -1749,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() } @@ -2111,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/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index a847f3a596..1e19cfb746 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -41,6 +41,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 @@ -76,6 +77,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 @@ -95,12 +98,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) @@ -1083,6 +1086,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("")) @@ -1483,6 +1502,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) @@ -3007,6 +3049,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/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/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index 1d0573741f..b9c5d62eec 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.setTrace(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.setTrace(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) + } + } + } } 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 } } From b0e83336c9fc94be67bc93ff213a0059dfd4380a Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 6 Dec 2024 16:40:32 +0100 Subject: [PATCH 05/14] Add rate limit for Continuous Profiling v8 (p6) (#3926) * continuous profiler now doesn't start if offline or rate limited * continuous profiler stops when rate limited * continuous profiler prevents sending chunks after being closed * added profile_chunk rate limit * continuous profiler now reset its id when rate limited or offline --- .../api/sentry-android-core.api | 3 +- .../core/AndroidContinuousProfiler.java | 55 +++++++++++++- .../core/AndroidContinuousProfilerTest.kt | 75 +++++++++++++++++++ .../api/sentry-opentelemetry-bootstrap.api | 2 +- sentry/api/sentry.api | 1 + .../src/main/java/io/sentry/DataCategory.java | 1 + .../clientreport/ClientReportRecorder.java | 3 + .../java/io/sentry/transport/RateLimiter.java | 2 + .../io/sentry/transport/RateLimiterTest.kt | 23 +++++- 9 files changed, 160 insertions(+), 5 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index dd4cfdf353..b1b4535796 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -36,11 +36,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } -public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler { +public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { 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 onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V public fun start ()V public fun stop ()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 0536dcabc7..99c042e015 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 @@ -1,10 +1,13 @@ package io.sentry.android.core; +import static io.sentry.DataCategory.All; +import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; import static java.util.concurrent.TimeUnit.SECONDS; import android.annotation.SuppressLint; import android.os.Build; import io.sentry.CompositePerformanceCollector; +import io.sentry.DataCategory; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.IScopes; @@ -17,17 +20,20 @@ import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.SentryId; +import io.sentry.transport.RateLimiter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; @ApiStatus.Internal -public class AndroidContinuousProfiler implements IContinuousProfiler { +public class AndroidContinuousProfiler + implements IContinuousProfiler, RateLimiter.IRateLimitObserver { private static final long MAX_CHUNK_DURATION_MILLIS = 10000; private final @NotNull ILogger logger; @@ -45,6 +51,7 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private final @NotNull List payloadBuilders = new ArrayList<>(); private @NotNull SentryId profilerId = SentryId.EMPTY_ID; private @NotNull SentryId chunkId = SentryId.EMPTY_ID; + private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -91,11 +98,15 @@ private void init() { } public synchronized void start() { - if ((scopes == null || scopes != NoOpScopes.getInstance()) + if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.getCurrentScopes(); this.performanceCollector = Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector(); + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null) { + rateLimiter.addRateLimitObserver(this); + } } // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler @@ -109,6 +120,26 @@ public synchronized void start() { return; } + if (scopes != null) { + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null + && (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + // Let's stop and reset profiler id, as the profile is now broken anyway + stop(); + return; + } + + // If device is offline, we don't start the profiler, to avoid flooding the cache + if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) { + logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler."); + // Let's stop and reset profiler id, as the profile is now broken anyway + stop(); + return; + } + } + final AndroidProfiler.ProfileStartData startData = profiler.start(); // check if profiling started if (startData == null) { @@ -150,6 +181,9 @@ private synchronized void stop(final boolean restartProfiler) { } // check if profiler was created and it's running if (profiler == null || !isRunning) { + // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the ids + profilerId = SentryId.EMPTY_ID; + chunkId = SentryId.EMPTY_ID; return; } @@ -203,6 +237,7 @@ private synchronized void stop(final boolean restartProfiler) { public synchronized void close() { stop(); + isClosed.set(true); } @Override @@ -216,6 +251,10 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti .getExecutorService() .submit( () -> { + // SDK is closed, we don't send the chunks + if (isClosed.get()) { + return; + } final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); synchronized (payloadBuilders) { for (ProfileChunk.Builder builder : payloadBuilders) { @@ -242,4 +281,16 @@ public boolean isRunning() { Future getStopFuture() { return stopFuture; } + + @Override + public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { + // We stop the profiler as soon as we are rate limited, to avoid the performance overhead + if (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + stop(); + } + // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's + // useless to restart it automatically + } } 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 70ed75186c..8093b2ee8b 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 @@ -6,6 +6,8 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.CompositePerformanceCollector import io.sentry.CpuCollectionData +import io.sentry.DataCategory +import io.sentry.IConnectionStatusProvider import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ISentryExecutorService @@ -18,8 +20,10 @@ import io.sentry.SentryTracer import io.sentry.TransactionContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector import io.sentry.profilemeasurements.ProfileMeasurement +import io.sentry.protocol.SentryId import io.sentry.test.DeferredExecutorService import io.sentry.test.getProperty +import io.sentry.transport.RateLimiter import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.check @@ -37,6 +41,7 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContains +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -394,4 +399,74 @@ class AndroidContinuousProfilerTest { executorService.runAll() verify(fixture.scopes, times(2)).captureProfileChunk(any()) } + + @Test + fun `profiler does not send chunks after close`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + + // We close the profiler, which should prevent sending additional chunks + profiler.close() + + // The executor used to send the chunk doesn't do anything + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + } + + @Test + fun `profiler stops when rate limited`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + + profiler.start() + assertTrue(profiler.isRunning) + + // If the SDK is rate limited, the profiler should stop + profiler.onRateLimitChanged(rateLimiter) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) + } + + @Test + fun `profiler does not start when rate limited`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) + + // If the SDK is rate limited, the profiler should never start + profiler.start() + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) + } + + @Test + fun `profiler does not start when offline`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + it.connectionStatusProvider = mock { provider -> + whenever(provider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) + } + } + + // If the device is offline, the profiler should never start + profiler.start() + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 0e061c84c0..ccc352dd03 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -41,7 +41,7 @@ public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFact public fun ()V public fun (Lio/opentelemetry/api/OpenTelemetry;)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/OtelTransactionSpanForwarder : io/sentry/ITransaction { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7ff8b12167..7fa2abafa7 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -341,6 +341,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Error Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; + public static final field ProfileChunk Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; public static final field Session Lio/sentry/DataCategory; diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index 43181eaeac..bbee8f2e84 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -12,6 +12,7 @@ public enum DataCategory { Attachment("attachment"), Monitor("monitor"), Profile("profile"), + ProfileChunk("profile_chunk"), Transaction("transaction"), Replay("replay"), Span("span"), diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index b4d4574aba..fae019f464 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -165,6 +165,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.Profile.equals(itemType)) { return DataCategory.Profile; } + if (SentryItemType.ProfileChunk.equals(itemType)) { + return DataCategory.ProfileChunk; + } if (SentryItemType.Attachment.equals(itemType)) { return DataCategory.Attachment; } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index cb560aff99..e8ac92e0bc 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -184,6 +184,8 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Attachment; case "profile": return DataCategory.Profile; + case "profile_chunk": + return DataCategory.ProfileChunk; case "transaction": return DataCategory.Transaction; case "check_in": diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 1b7ae7fe61..9cc24fcabc 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -10,6 +10,7 @@ import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ISerializer import io.sentry.NoOpLogger +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.ReplayRecording import io.sentry.SentryEnvelope @@ -208,8 +209,9 @@ class RateLimiterTest { val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val checkInItem = SentryEnvelopeItem.fromCheckIn(fixture.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) + val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, profileChunkItem)) rateLimiter.updateRetryAfterLimits(null, null, 429) val result = rateLimiter.filter(envelope, Hint()) @@ -222,6 +224,7 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(checkInItem)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } @@ -331,6 +334,24 @@ class RateLimiterTest { verifyNoMoreInteractions(fixture.clientReportRecorder) } + @Test + fun `drop profileChunk items as lost`() { + val rateLimiter = fixture.getSUT() + + val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) + val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(profileChunkItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:profile_chunk:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + @Test fun `apply rate limits notifies observers`() { val rateLimiter = fixture.getSUT() From e70d583e4792bb443d671010644e5fd0957a0b7c Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 23 Dec 2024 19:43:52 +0100 Subject: [PATCH 06/14] added SentryOptions.continuousProfilesSampleRate (#4013) * added SentryOptions.continuousProfilesSampleRate * now continuous profiling is disabled if continuousProfilesSampleRate is 0 * profiles directory is created when continuous profiling is enabled, too * continuous profiling decision is passed to SentryAppStartProfilingOptions * app start continuous profiling is sampled, too --- .../android/core/ManifestMetadataReader.java | 11 ++++ .../core/SentryPerformanceProvider.java | 6 ++ .../core/ManifestMetadataReaderTest.kt | 41 ++++++++++++++ .../core/SentryPerformanceProviderTest.kt | 15 +++++ sentry/api/sentry.api | 9 +++ .../main/java/io/sentry/ExternalOptions.java | 11 ++++ sentry/src/main/java/io/sentry/Scopes.java | 12 +++- sentry/src/main/java/io/sentry/Sentry.java | 3 +- .../SentryAppStartProfilingOptions.java | 19 +++++++ .../main/java/io/sentry/SentryOptions.java | 55 ++++++++++++++++--- .../main/java/io/sentry/TracesSampler.java | 5 ++ .../java/io/sentry/util/SampleRateUtils.java | 4 ++ .../java/io/sentry/ExternalOptionsTest.kt | 7 +++ .../test/java/io/sentry/JsonSerializerTest.kt | 11 ++-- sentry/src/test/java/io/sentry/ScopesTest.kt | 19 +++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 39 ++++++++++++- sentry/src/test/java/io/sentry/SentryTest.kt | 44 +++++++++++++++ .../test/java/io/sentry/TracesSamplerTest.kt | 25 +++++++++ .../java/io/sentry/util/SampleRateUtilTest.kt | 35 ++++++++++++ 19 files changed, 354 insertions(+), 17 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 45661866fa..f49291340f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -64,6 +64,9 @@ final class ManifestMetadataReader { static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate"; + static final String CONTINUOUS_PROFILES_SAMPLE_RATE = + "io.sentry.traces.profiling.continuous-sample-rate"; + @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; @@ -315,6 +318,14 @@ static void applyMetadata( } } + if (options.getContinuousProfilesSampleRate() == 1.0) { + final double continuousProfilesSampleRate = + readDouble(metadata, logger, CONTINUOUS_PROFILES_SAMPLE_RATE); + if (continuousProfilesSampleRate != -1) { + options.setContinuousProfilesSampleRate(continuousProfilesSampleRate); + } + } + options.setEnableUserInteractionTracing( readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); 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 1d76775b3f..583f984cc8 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 @@ -162,6 +162,12 @@ private void createAndStartContinuousProfiler( final @NotNull Context context, final @NotNull SentryAppStartProfilingOptions profilingOptions, final @NotNull AppStartMetrics appStartMetrics) { + + if (!profilingOptions.isContinuousProfileSampled()) { + logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); + return; + } + final @NotNull IContinuousProfiler appStartContinuousProfiler = new AndroidContinuousProfiler( buildInfoProvider, diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 5d6e915c0b..20900ea133 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -800,6 +800,47 @@ class ManifestMetadataReaderTest { assertNull(fixture.options.profilesSampleRate) } + @Test + fun `applyMetadata reads continuousProfilesSampleRate from metadata`() { + // Arrange + val expectedSampleRate = 0.99f + val bundle = bundleOf(ManifestMetadataReader.CONTINUOUS_PROFILES_SAMPLE_RATE to expectedSampleRate) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options.continuousProfilesSampleRate) + } + + @Test + fun `applyMetadata does not override continuousProfilesSampleRate from options`() { + // Arrange + val expectedSampleRate = 0.99f + fixture.options.continuousProfilesSampleRate = expectedSampleRate.toDouble() + val bundle = bundleOf(ManifestMetadataReader.CONTINUOUS_PROFILES_SAMPLE_RATE to 0.1f) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options.continuousProfilesSampleRate) + } + + @Test + fun `applyMetadata without specifying continuousProfilesSampleRate, stays 1`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(1.0, fixture.options.continuousProfilesSampleRate) + } + @Test fun `applyMetadata reads tracePropagationTargets to options`() { // Arrange 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 237bc54867..16cce1a209 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 @@ -259,6 +259,19 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when continuous profile is not sampled, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, continuousProfileSampled = false) + } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start profiling was not sampled. It will not start.") + ) + } + // This case should never happen in reality, but it's technically possible to have such configuration @Test fun `when both transaction and continuous profilers are enabled, only continuous profiler is created`() { @@ -331,6 +344,7 @@ class SentryPerformanceProviderTest { traceSampleRate: Double = 1.0, profileSampled: Boolean = true, profileSampleRate: Double = 1.0, + continuousProfileSampled: Boolean = true, profilingTracesDirPath: String = traceDir.absolutePath ) { val appStartProfilingOptions = SentryAppStartProfilingOptions() @@ -340,6 +354,7 @@ class SentryPerformanceProviderTest { appStartProfilingOptions.traceSampleRate = traceSampleRate appStartProfilingOptions.isProfileSampled = profileSampled appStartProfilingOptions.profileSampleRate = profileSampleRate + appStartProfilingOptions.isContinuousProfileSampled = continuousProfileSampled appStartProfilingOptions.profilingTracesDirPath = profilingTracesDirPath appStartProfilingOptions.profilingTracesHz = 101 JsonSerializer(SentryOptions.empty()).serialize(appStartProfilingOptions, FileWriter(configFile)) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7fa2abafa7..a097588a7c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -458,6 +458,7 @@ public final class io/sentry/ExternalOptions { public static fun from (Lio/sentry/config/PropertiesProvider;Lio/sentry/ILogger;)Lio/sentry/ExternalOptions; public fun getBundleIds ()Ljava/util/Set; public fun getContextTags ()Ljava/util/List; + public fun getContinuousProfilesSampleRate ()Ljava/lang/Double; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDebug ()Ljava/lang/Boolean; public fun getDist ()Ljava/lang/String; @@ -491,6 +492,7 @@ public final class io/sentry/ExternalOptions { public fun isGlobalHubMode ()Ljava/lang/Boolean; public fun isSendDefaultPii ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; + public fun setContinuousProfilesSampleRate (Ljava/lang/Double;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V @@ -2488,11 +2490,13 @@ 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 isContinuousProfileSampled ()Z 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 setContinuousProfileSampled (Z)V public fun setContinuousProfilingEnabled (Z)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V @@ -2511,6 +2515,7 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; 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; @@ -2944,6 +2949,7 @@ public class io/sentry/SentryOptions { public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; + public fun getContinuousProfilesSampleRate ()D public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; @@ -3060,6 +3066,7 @@ public class io/sentry/SentryOptions { public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V + public fun setContinuousProfilesSampleRate (D)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -3759,6 +3766,7 @@ public final class io/sentry/TraceContext$JsonKeys { public final class io/sentry/TracesSampler { public fun (Lio/sentry/SentryOptions;)V public fun sample (Lio/sentry/SamplingContext;)Lio/sentry/TracesSamplingDecision; + public fun sampleContinuousProfile ()Z } public final class io/sentry/TracesSamplingDecision { @@ -6316,6 +6324,7 @@ public final class io/sentry/util/Random : java/io/Serializable { public final class io/sentry/util/SampleRateUtils { public fun ()V + public static fun isValidContinuousProfilesSampleRate (D)Z public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index d9b075e1c8..020b7aea9f 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -28,6 +28,7 @@ public final class ExternalOptions { private @Nullable Boolean enableDeduplication; private @Nullable Double tracesSampleRate; private @Nullable Double profilesSampleRate; + private @Nullable Double continuousProfilesSampleRate; private @Nullable SentryOptions.RequestSize maxRequestBodySize; private final @NotNull Map tags = new ConcurrentHashMap<>(); private @Nullable SentryOptions.Proxy proxy; @@ -73,6 +74,8 @@ public final class ExternalOptions { propertiesProvider.getBooleanProperty("uncaught.handler.print-stacktrace")); options.setTracesSampleRate(propertiesProvider.getDoubleProperty("traces-sample-rate")); options.setProfilesSampleRate(propertiesProvider.getDoubleProperty("profiles-sample-rate")); + options.setContinuousProfilesSampleRate( + propertiesProvider.getDoubleProperty("continuous-profiles-sample-rate")); options.setDebug(propertiesProvider.getBooleanProperty("debug")); options.setEnableDeduplication(propertiesProvider.getBooleanProperty("enable-deduplication")); options.setSendClientReports(propertiesProvider.getBooleanProperty("send-client-reports")); @@ -284,6 +287,14 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { this.profilesSampleRate = profilesSampleRate; } + public @Nullable Double getContinuousProfilesSampleRate() { + return continuousProfilesSampleRate; + } + + public void setContinuousProfilesSampleRate(final @Nullable Double continuousProfilesSampleRate) { + this.continuousProfilesSampleRate = continuousProfilesSampleRate; + } + public @Nullable SentryOptions.RequestSize getMaxRequestBodySize() { return maxRequestBodySize; } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 86f0f5f8bc..638127cc54 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -926,9 +926,15 @@ public void flush(long timeoutMillis) { @Override public void startProfiler() { if (getOptions().isContinuousProfilingEnabled()) { - getOptions().getLogger().log(SentryLevel.DEBUG, "Started continuous Profiling."); - getOptions().getContinuousProfiler().start(); - } else { + if (getOptions().getInternalTracesSampler().sampleContinuousProfile()) { + getOptions().getLogger().log(SentryLevel.DEBUG, "Started continuous Profiling."); + getOptions().getContinuousProfiler().start(); + } else { + getOptions() + .getLogger() + .log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); + } + } else if (getOptions().isProfilingEnabled()) { getOptions() .getLogger() .log( diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 8e8f8c5b65..383ba50e84 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -514,7 +514,8 @@ private static void initConfigurations(final @NotNull SentryOptions options) { } final String profilingTracesDirPath = options.getProfilingTracesDirPath(); - if (options.isProfilingEnabled() && profilingTracesDirPath != null) { + if ((options.isProfilingEnabled() || options.isContinuousProfilingEnabled()) + && profilingTracesDirPath != null) { final File profilingTracesDir = new File(profilingTracesDirPath); profilingTracesDir.mkdirs(); diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index b0926b9d93..c4d99a60f2 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -20,6 +20,7 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe boolean isProfilingEnabled; boolean isContinuousProfilingEnabled; int profilingTracesHz; + boolean continuousProfileSampled; private @Nullable Map unknown; @@ -29,6 +30,7 @@ public SentryAppStartProfilingOptions() { traceSampleRate = null; profileSampled = false; profileSampleRate = null; + continuousProfileSampled = false; profilingTracesDirPath = null; isProfilingEnabled = false; isContinuousProfilingEnabled = false; @@ -42,6 +44,7 @@ public SentryAppStartProfilingOptions() { traceSampleRate = samplingDecision.getSampleRate(); profileSampled = samplingDecision.getProfileSampled(); profileSampleRate = samplingDecision.getProfileSampleRate(); + continuousProfileSampled = options.getInternalTracesSampler().sampleContinuousProfile(); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); @@ -56,6 +59,14 @@ public boolean isProfileSampled() { return profileSampled; } + public void setContinuousProfileSampled(boolean continuousProfileSampled) { + this.continuousProfileSampled = continuousProfileSampled; + } + + public boolean isContinuousProfileSampled() { + return continuousProfileSampled; + } + public void setProfileSampleRate(final @Nullable Double profileSampleRate) { this.profileSampleRate = profileSampleRate; } @@ -117,6 +128,7 @@ public int getProfilingTracesHz() { public static final class JsonKeys { public static final String PROFILE_SAMPLED = "profile_sampled"; public static final String PROFILE_SAMPLE_RATE = "profile_sample_rate"; + public static final String CONTINUOUS_PROFILE_SAMPLED = "continuous_profile_sampled"; public static final String TRACE_SAMPLED = "trace_sampled"; public static final String TRACE_SAMPLE_RATE = "trace_sample_rate"; public static final String PROFILING_TRACES_DIR_PATH = "profiling_traces_dir_path"; @@ -131,6 +143,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.beginObject(); writer.name(JsonKeys.PROFILE_SAMPLED).value(logger, profileSampled); writer.name(JsonKeys.PROFILE_SAMPLE_RATE).value(logger, profileSampleRate); + writer.name(JsonKeys.CONTINUOUS_PROFILE_SAMPLED).value(logger, continuousProfileSampled); writer.name(JsonKeys.TRACE_SAMPLED).value(logger, traceSampled); writer.name(JsonKeys.TRACE_SAMPLE_RATE).value(logger, traceSampleRate); writer.name(JsonKeys.PROFILING_TRACES_DIR_PATH).value(logger, profilingTracesDirPath); @@ -186,6 +199,12 @@ public static final class Deserializer options.profileSampleRate = profileSampleRate; } break; + case JsonKeys.CONTINUOUS_PROFILE_SAMPLED: + Boolean continuousProfileSampled = reader.nextBooleanOrNull(); + if (continuousProfileSampled != null) { + options.continuousProfileSampled = continuousProfileSampled; + } + break; case JsonKeys.TRACE_SAMPLED: Boolean traceSampled = reader.nextBooleanOrNull(); if (traceSampled != null) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index cacf4a77fc..8418d4cea4 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -357,6 +357,15 @@ public class SentryOptions { */ private @Nullable ProfilesSamplerCallback profilesSampler; + /** + * Configures the continuous profiling sample rate as a percentage of profiles to be sent in the + * range of 0.0 to 1.0. if 1.0 is set it means that 100% of profiles will be sent. If set to 0.1 + * only 10% of profiles will be sent. Profiles are picked randomly. Default is 1 (100%). + * ProfilesSampleRate takes precedence over this. To enable continuous profiling, don't set + * profilesSampleRate or profilesSampler, or set them to null. + */ + private double continuousProfilesSampleRate = 1.0; + /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; @@ -481,7 +490,10 @@ public class SentryOptions { private boolean enableBackpressureHandling = true; - /** Whether to profile app launches, depending on profilesSampler or profilesSampleRate. */ + /** + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. + */ private boolean enableAppStartProfiling = false; private @NotNull ISpanFactory spanFactory = NoOpSpanFactory.getInstance(); @@ -1724,8 +1736,7 @@ public void setContinuousProfiler(final @Nullable IContinuousProfiler continuous * @return if profiling is enabled for transactions. */ public boolean isProfilingEnabled() { - return (getProfilesSampleRate() != null && getProfilesSampleRate() > 0) - || getProfilesSampler() != null; + return (profilesSampleRate != null && profilesSampleRate > 0) || profilesSampler != null; } /** @@ -1736,7 +1747,9 @@ public boolean isProfilingEnabled() { */ @ApiStatus.Internal public boolean isContinuousProfilingEnabled() { - return getProfilesSampleRate() == null && getProfilesSampler() == null; + return profilesSampleRate == null + && profilesSampler == null + && continuousProfilesSampleRate > 0; } /** @@ -1783,6 +1796,27 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { this.profilesSampleRate = profilesSampleRate; } + /** + * Returns the continuous profiling sample rate. Default is 1 (100%). ProfilesSampleRate takes + * precedence over this. To enable continuous profiling, don't set profilesSampleRate or + * profilesSampler, or set them to null. + * + * @return the sample rate + */ + public double getContinuousProfilesSampleRate() { + return continuousProfilesSampleRate; + } + + public void setContinuousProfilesSampleRate(final double continuousProfilesSampleRate) { + if (!SampleRateUtils.isValidContinuousProfilesSampleRate(continuousProfilesSampleRate)) { + throw new IllegalArgumentException( + "The value " + + continuousProfilesSampleRate + + " is not valid. Use values between 0.0 and 1.0."); + } + this.continuousProfilesSampleRate = continuousProfilesSampleRate; + } + /** * Returns the profiling traces dir. path if set * @@ -2181,17 +2215,19 @@ public void setEnablePrettySerializationOutput(boolean enablePrettySerialization } /** - * Whether to profile app launches, depending on profilesSampler or profilesSampleRate. Depends on - * {@link SentryOptions#isProfilingEnabled()} + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. Depends on {@link SentryOptions#isProfilingEnabled()} and {@link + * SentryOptions#isContinuousProfilingEnabled()} * * @return true if app launches should be profiled. */ public boolean isEnableAppStartProfiling() { - return isProfilingEnabled() && enableAppStartProfiling; + return (isProfilingEnabled() || isContinuousProfilingEnabled()) && enableAppStartProfiling; } /** - * Whether to profile app launches, depending on profilesSampler or profilesSampleRate. + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. * * @param enableAppStartProfiling true if app launches should be profiled. */ @@ -2707,6 +2743,9 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getProfilesSampleRate() != null) { setProfilesSampleRate(options.getProfilesSampleRate()); } + if (options.getContinuousProfilesSampleRate() != null) { + setContinuousProfilesSampleRate(options.getContinuousProfilesSampleRate()); + } if (options.getDebug() != null) { setDebug(options.getDebug()); } diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index 3ce28ab745..fcf6e3929a 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -85,6 +85,11 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont return new TracesSamplingDecision(false, null, false, null); } + public boolean sampleContinuousProfile() { + final double sampling = options.getContinuousProfilesSampleRate(); + return sample(sampling); + } + private boolean sample(final @NotNull Double aDouble) { return !(aDouble < getRandom().nextDouble()); } diff --git a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java index ed011ff842..ab84cd0e9d 100644 --- a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java +++ b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java @@ -23,6 +23,10 @@ public static boolean isValidProfilesSampleRate(@Nullable Double profilesSampleR return isValidRate(profilesSampleRate, true); } + public static boolean isValidContinuousProfilesSampleRate(double profilesSampleRate) { + return isValidRate(profilesSampleRate, false); + } + private static boolean isValidRate(final @Nullable Double rate, final boolean allowNull) { if (rate == null) { return allowNull; diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index b25f67405c..3a1d66b264 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -120,6 +120,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with continuousProfilesSampleRate using external properties`() { + withPropertiesFile("continuous-profiles-sample-rate=0.2") { + assertEquals(0.2, it.continuousProfilesSampleRate) + } + } + @Test fun `creates options with enableDeduplication using external properties`() { withPropertiesFile("enable-deduplication=true") { diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 53dd7d85bf..7b8fc219b9 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1229,9 +1229,9 @@ class JsonSerializerTest { fun `serializing SentryAppStartProfilingOptions`() { 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," + - "\"is_continuous_profiling_enabled\":false,\"profiling_traces_hz\":65}" + val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"continuous_profile_sampled\":true," + + "\"trace_sampled\":false,\"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) } @@ -1239,7 +1239,8 @@ class JsonSerializerTest { @Test fun `deserializing SentryAppStartProfilingOptions`() { val jsonAppStartProfilingOptions = "{\"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}" + ":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"profiling_traces_hz\":65,\"continuous_profile_sampled\":true}" val actual = fixture.serializer.deserialize(StringReader(jsonAppStartProfilingOptions), SentryAppStartProfilingOptions::class.java) assertNotNull(actual) @@ -1247,6 +1248,7 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.traceSampleRate, actual.traceSampleRate) assertEquals(appStartProfilingOptions.profileSampled, actual.profileSampled) assertEquals(appStartProfilingOptions.profileSampleRate, actual.profileSampleRate) + assertEquals(appStartProfilingOptions.continuousProfileSampled, actual.isContinuousProfileSampled) assertEquals(appStartProfilingOptions.isProfilingEnabled, actual.isProfilingEnabled) assertEquals(appStartProfilingOptions.isContinuousProfilingEnabled, actual.isContinuousProfilingEnabled) assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) @@ -1549,6 +1551,7 @@ class JsonSerializerTest { private val appStartProfilingOptions = SentryAppStartProfilingOptions().apply { traceSampled = false traceSampleRate = 0.1 + continuousProfileSampled = true profileSampled = true profileSampleRate = 0.8 isProfilingEnabled = false diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 68a1fee5d6..107d9375a4 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -15,6 +15,7 @@ import io.sentry.test.callMethod import io.sentry.test.createSentryClientMock import io.sentry.test.createTestScopes import io.sentry.util.HintUtils +import io.sentry.util.SentryRandom import io.sentry.util.StringUtils import junit.framework.TestCase.assertSame import org.mockito.kotlin.any @@ -2170,6 +2171,24 @@ class ScopesTest { verify(profiler).start() } + @Test + fun `startProfiler logs a message if not sampled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.continuousProfilesSampleRate = 0.1 + it.setLogger(logger) + it.isDebug = true + } + // We cannot set sample rate to 0, as it would not start the profiler. So we set the seed to have consistent results + SentryRandom.current().setSeed(0) + scopes.startProfiler() + + verify(profiler, never()).start() + verify(logger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) + } + @Test fun `stopProfiler stops the continuous profiler`() { val profiler = mock() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index afe962d19f..03146f8180 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -236,6 +236,15 @@ class SentryOptionsTest { assertFalse(options.isContinuousProfilingEnabled) } + @Test + fun `when continuousProfilesSampleRate is set to a 0, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { + val options = SentryOptions().apply { + this.continuousProfilesSampleRate = 0.0 + } + assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) + } + @Test fun `when setProfilesSampleRate is set to exactly 0, value is set`() { val options = SentryOptions().apply { @@ -254,6 +263,24 @@ class SentryOptionsTest { assertFailsWith { SentryOptions().profilesSampleRate = -0.0000000000001 } } + @Test + fun `when setContinuousProfilesSampleRate is set to exactly 0, value is set`() { + val options = SentryOptions().apply { + this.continuousProfilesSampleRate = 0.0 + } + assertEquals(0.0, options.continuousProfilesSampleRate) + } + + @Test + fun `when setContinuousProfilesSampleRate is set to higher than 1_0, setter throws`() { + assertFailsWith { SentryOptions().continuousProfilesSampleRate = 1.0000000000001 } + } + + @Test + fun `when setContinuousProfilesSampleRate is set to lower than 0, setter throws`() { + assertFailsWith { SentryOptions().continuousProfilesSampleRate = -0.0000000000001 } + } + @Test fun `when options is initialized, compositePerformanceCollector is set`() { assertIs(SentryOptions().compositePerformanceCollector) @@ -322,6 +349,7 @@ class SentryOptionsTest { externalOptions.enableUncaughtExceptionHandler = false externalOptions.tracesSampleRate = 0.5 externalOptions.profilesSampleRate = 0.5 + externalOptions.continuousProfilesSampleRate = 0.3 externalOptions.addInAppInclude("com.app") externalOptions.addInAppExclude("io.off") externalOptions.addTracePropagationTarget("localhost") @@ -368,6 +396,7 @@ class SentryOptionsTest { assertFalse(options.isEnableUncaughtExceptionHandler) assertEquals(0.5, options.tracesSampleRate) assertEquals(0.5, options.profilesSampleRate) + assertEquals(0.3, options.continuousProfilesSampleRate) assertEquals(listOf("com.app"), options.inAppIncludes) assertEquals(listOf("io.off"), options.inAppExcludes) assertEquals(listOf("localhost", "api.foo.com"), options.tracePropagationTargets) @@ -578,10 +607,18 @@ class SentryOptionsTest { fun `when profiling is disabled, isEnableAppStartProfiling is always false`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.profilesSampleRate = 0.0 + options.continuousProfilesSampleRate = 0.0 assertFalse(options.isEnableAppStartProfiling) } + @Test + fun `when setEnableAppStartProfiling is called and continuous profiling is enabled, isEnableAppStartProfiling is true`() { + val options = SentryOptions() + options.isEnableAppStartProfiling = true + options.continuousProfilesSampleRate = 1.0 + assertTrue(options.isEnableAppStartProfiling) + } + @Test fun `when options are initialized, profilingTracesHz is set to 101 by default`() { assertEquals(101, SentryOptions().profilingTracesHz) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 6f7cd427da..ae0243618a 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -18,6 +18,7 @@ import io.sentry.test.ImmediateExecutorService import io.sentry.test.createSentryClientMock import io.sentry.test.injectForField import io.sentry.util.PlatformTestManipulator +import io.sentry.util.SentryRandom import io.sentry.util.thread.IThreadChecker import io.sentry.util.thread.ThreadChecker import org.awaitility.kotlin.await @@ -400,6 +401,35 @@ class SentryTest { assertTrue(File(sentryOptions?.profilingTracesDirPath!!).list()!!.isEmpty()) } + @Test + fun `profilingTracesDirPath should be created and cleared at initialization when continuous profiling is enabled`() { + val tempPath = getTempPath() + var sentryOptions: SentryOptions? = null + Sentry.init { + it.dsn = dsn + it.continuousProfilesSampleRate = 1.0 + it.cacheDirPath = tempPath + sentryOptions = it + } + + assertTrue(File(sentryOptions?.profilingTracesDirPath!!).exists()) + assertTrue(File(sentryOptions?.profilingTracesDirPath!!).list()!!.isEmpty()) + } + + @Test + fun `profilingTracesDirPath should not be created when no profiling is enabled`() { + val tempPath = getTempPath() + var sentryOptions: SentryOptions? = null + Sentry.init { + it.dsn = dsn + it.continuousProfilesSampleRate = 0.0 + it.cacheDirPath = tempPath + sentryOptions = it + } + + assertFalse(File(sentryOptions?.profilingTracesDirPath!!).exists()) + } + @Test fun `only old profiles in profilingTracesDirPath should be cleared when profiling is enabled`() { val tempPath = getTempPath() @@ -1300,6 +1330,20 @@ class SentryTest { verify(profiler, never()).start() } + @Test + fun `startProfiler is ignored when not sampled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.continuousProfilesSampleRate = 0.1 + } + // We cannot set sample rate to 0, as it would not start the profiler. So we set the seed to have consistent results + SentryRandom.current().setSeed(0) + Sentry.startProfiler() + verify(profiler, never()).start() + } + @Test fun `stopProfiler stops the continuous profiler`() { val profiler = mock() diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 06eb60aece..4523a6ecd1 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -18,6 +18,7 @@ class TracesSamplerTest { randomResult: Double? = null, tracesSampleRate: Double? = null, profilesSampleRate: Double? = null, + continuousProfilesSampleRate: Double? = null, tracesSamplerCallback: SentryOptions.TracesSamplerCallback? = null, profilesSamplerCallback: SentryOptions.ProfilesSamplerCallback? = null, logger: ILogger? = null @@ -33,6 +34,9 @@ class TracesSamplerTest { if (profilesSampleRate != null) { options.profilesSampleRate = profilesSampleRate } + if (continuousProfilesSampleRate != null) { + options.continuousProfilesSampleRate = continuousProfilesSampleRate + } if (tracesSamplerCallback != null) { options.tracesSampler = tracesSamplerCallback } @@ -150,6 +154,27 @@ class TracesSamplerTest { assertEquals(0.2, samplingDecision.profileSampleRate) } + @Test + fun `when continuousProfilesSampleRate is not set returns true`() { + val sampler = fixture.getSut(randomResult = 1.0) + val sampled = sampler.sampleContinuousProfile() + assertTrue(sampled) + } + + @Test + fun `when continuousProfilesSampleRate is set and random returns lower number returns true`() { + val sampler = fixture.getSut(randomResult = 0.1, continuousProfilesSampleRate = 0.2) + val sampled = sampler.sampleContinuousProfile() + assertTrue(sampled) + } + + @Test + fun `when continuousProfilesSampleRate is set and random returns greater number returns false`() { + val sampler = fixture.getSut(randomResult = 0.9, continuousProfilesSampleRate = 0.2) + val sampled = sampler.sampleContinuousProfile() + assertFalse(sampled) + } + @Test fun `when tracesSampler returns null and parentSampled is set sampler uses it as a sampling decision`() { val sampler = fixture.getSut(tracesSamplerCallback = null) diff --git a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt index e5c81bc70e..3a14f2bec5 100644 --- a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt +++ b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt @@ -130,4 +130,39 @@ class SampleRateUtilTest { fun `accepts null profiles sample rate`() { assertTrue(SampleRateUtils.isValidProfilesSampleRate(null)) } + + @Test + fun `accepts 0 for continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidContinuousProfilesSampleRate(0.0)) + } + + @Test + fun `accepts 1 for continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidContinuousProfilesSampleRate(1.0)) + } + + @Test + fun `rejects negative continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(-0.5)) + } + + @Test + fun `rejects 1 dot 01 for continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(1.01)) + } + + @Test + fun `rejects NaN continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.NaN)) + } + + @Test + fun `rejects positive infinite continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.POSITIVE_INFINITY)) + } + + @Test + fun `rejects negative infinite continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.NEGATIVE_INFINITY)) + } } From 87bdd6d49754541860e68a65d1dd3ee13082e1a9 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 14 Feb 2025 10:37:33 +0100 Subject: [PATCH 07/14] Wrap up continuous profiling (#4069) * Set continuousProfilesSampleRate and startProfiler() and stopProfiler() as experimental * Added chunk start timestamp to ProfileChunk * increased continuous profiling chunk duration to 1 minute --- CHANGELOG.md | 40 +++++++++++++++++++ .../core/AndroidContinuousProfiler.java | 15 +++++-- .../android/core/ManifestMetadataReader.java | 2 +- .../core/ManifestMetadataReaderTest.kt | 2 +- sentry/api/sentry.api | 9 +++-- .../java/io/sentry/ExperimentalOptions.java | 26 ++++++++++++ .../src/main/java/io/sentry/ProfileChunk.java | 23 ++++++++++- sentry/src/main/java/io/sentry/Sentry.java | 4 +- .../main/java/io/sentry/SentryOptions.java | 28 +++---------- .../test/java/io/sentry/JsonSerializerTest.kt | 5 ++- sentry/src/test/java/io/sentry/ScopesTest.kt | 2 +- .../test/java/io/sentry/SentryClientTest.kt | 2 +- .../test/java/io/sentry/SentryOptionsTest.kt | 12 +++--- sentry/src/test/java/io/sentry/SentryTest.kt | 6 +-- .../test/java/io/sentry/TracesSamplerTest.kt | 2 +- 15 files changed, 132 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbdbc0d7e..721f319790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## Unreleased + +### Features + +- Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710)) + + To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.continuousProfilesSampleRate` (defaults to 1.0). + Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling. + + ```java + import io.sentry.android.core.SentryAndroid; + + SentryAndroid.init(context) { options -> + + // Currently under experimental options: + options.getExperimental().setContinuousProfilesSampleRate(1.0); + } + // Start profiling + Sentry.startProfiler(); + + // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. + Sentry.stopProfiler(); + ``` + ```kotlin + import io.sentry.android.core.SentryAndroid + + SentryAndroid.init(context) { options -> + + // Currently under experimental options: + options.experimental.continuousProfilesSampleRate = 1.0 + } + // Start profiling + Sentry.startProfiler() + + // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. + Sentry.stopProfiler() + ``` + + To learn more visit [Sentry's Continuous Profiling](https://docs.sentry.io/product/explore/profiling/transaction-vs-continuous-profiling/#continuous-profiling-mode) documentation page. + ## 8.0.0-beta.3 ### Features 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 99c042e015..ac43ac53c3 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 @@ -16,7 +16,9 @@ import io.sentry.PerformanceCollectionData; import io.sentry.ProfileChunk; import io.sentry.Sentry; +import io.sentry.SentryDate; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.SentryId; @@ -34,7 +36,7 @@ @ApiStatus.Internal public class AndroidContinuousProfiler implements IContinuousProfiler, RateLimiter.IRateLimitObserver { - private static final long MAX_CHUNK_DURATION_MILLIS = 10000; + private static final long MAX_CHUNK_DURATION_MILLIS = 60000; private final @NotNull ILogger logger; private final @Nullable String profilingTracesDirPath; @@ -52,6 +54,7 @@ public class AndroidContinuousProfiler private @NotNull SentryId profilerId = SentryId.EMPTY_ID; private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); + private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -138,8 +141,10 @@ public synchronized void start() { stop(); return; } + startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now(); + } else { + startProfileChunkTimestamp = new SentryNanotimeDate(); } - final AndroidProfiler.ProfileStartData startData = profiler.start(); // check if profiling started if (startData == null) { @@ -213,7 +218,11 @@ private synchronized void stop(final boolean restartProfiler) { synchronized (payloadBuilders) { payloadBuilders.add( new ProfileChunk.Builder( - profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + profilerId, + chunkId, + endData.measurementsMap, + endData.traceFile, + startProfileChunkTimestamp)); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index f49291340f..70085b2356 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -322,7 +322,7 @@ static void applyMetadata( final double continuousProfilesSampleRate = readDouble(metadata, logger, CONTINUOUS_PROFILES_SAMPLE_RATE); if (continuousProfilesSampleRate != -1) { - options.setContinuousProfilesSampleRate(continuousProfilesSampleRate); + options.getExperimental().setContinuousProfilesSampleRate(continuousProfilesSampleRate); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 20900ea133..e1f08b396e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -818,7 +818,7 @@ class ManifestMetadataReaderTest { fun `applyMetadata does not override continuousProfilesSampleRate from options`() { // Arrange val expectedSampleRate = 0.99f - fixture.options.continuousProfilesSampleRate = expectedSampleRate.toDouble() + fixture.options.experimental.continuousProfilesSampleRate = expectedSampleRate.toDouble() val bundle = bundleOf(ManifestMetadataReader.CONTINUOUS_PROFILES_SAMPLE_RATE to 0.1f) val context = fixture.getContext(metaData = bundle) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a097588a7c..4d93e217f8 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -443,7 +443,9 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (Z)V + public fun getContinuousProfilesSampleRate ()D public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; + public fun setContinuousProfilesSampleRate (D)V public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V } @@ -1846,7 +1848,7 @@ public final class io/sentry/PerformanceCollectionData { 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 (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Ljava/lang/Double;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; @@ -1857,6 +1859,7 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getRelease ()Ljava/lang/String; public fun getSampledProfile ()Ljava/lang/String; + public fun getTimestamp ()D public fun getTraceFile ()Ljava/io/File; public fun getUnknown ()Ljava/util/Map; public fun getVersion ()Ljava/lang/String; @@ -1868,7 +1871,7 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr } 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 (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;Lio/sentry/SentryDate;)V public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; } @@ -1888,6 +1891,7 @@ public final class io/sentry/ProfileChunk$JsonKeys { 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 TIMESTAMP Ljava/lang/String; public static final field VERSION Ljava/lang/String; public fun ()V } @@ -3066,7 +3070,6 @@ public class io/sentry/SentryOptions { public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V - public fun setContinuousProfilesSampleRate (D)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 4a0e7de78d..1a473d7c46 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -1,5 +1,7 @@ package io.sentry; +import io.sentry.util.SampleRateUtils; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -11,6 +13,15 @@ public final class ExperimentalOptions { private @NotNull SentryReplayOptions sessionReplay; + /** + * Configures the continuous profiling sample rate as a percentage of profiles to be sent in the + * range of 0.0 to 1.0. if 1.0 is set it means that 100% of profiles will be sent. If set to 0.1 + * only 10% of profiles will be sent. Profiles are picked randomly. Default is 1 (100%). + * ProfilesSampleRate takes precedence over this. To enable continuous profiling, don't set + * profilesSampleRate or profilesSampler, or set them to null. + */ + private double continuousProfilesSampleRate = 1.0; + public ExperimentalOptions(final boolean empty) { this.sessionReplay = new SentryReplayOptions(empty); } @@ -23,4 +34,19 @@ public SentryReplayOptions getSessionReplay() { public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOptions) { this.sessionReplay = sessionReplayOptions; } + + public double getContinuousProfilesSampleRate() { + return continuousProfilesSampleRate; + } + + @ApiStatus.Experimental + public void setContinuousProfilesSampleRate(final double continuousProfilesSampleRate) { + if (!SampleRateUtils.isValidContinuousProfilesSampleRate(continuousProfilesSampleRate)) { + throw new IllegalArgumentException( + "The value " + + continuousProfilesSampleRate + + " is not valid. Use values between 0.0 and 1.0."); + } + this.continuousProfilesSampleRate = continuousProfilesSampleRate; + } } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 725c151dbd..89d9293f5c 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -26,6 +26,7 @@ public final class ProfileChunk implements JsonUnknown, JsonSerializable { private @NotNull String release; private @Nullable String environment; private @NotNull String version; + private double timestamp; private final @NotNull File traceFile; @@ -40,6 +41,7 @@ public ProfileChunk() { SentryId.EMPTY_ID, new File("dummy"), new HashMap<>(), + 0.0, SentryOptions.empty()); } @@ -48,6 +50,7 @@ public ProfileChunk( final @NotNull SentryId chunkId, final @NotNull File traceFile, final @NotNull Map measurements, + final @NotNull Double timestamp, final @NotNull SentryOptions options) { this.profilerId = profilerId; this.chunkId = chunkId; @@ -59,6 +62,7 @@ public ProfileChunk( this.environment = options.getEnvironment(); this.platform = "android"; this.version = "2"; + this.timestamp = timestamp; } public @NotNull Map getMeasurements() { @@ -109,6 +113,10 @@ public void setSampledProfile(final @Nullable String sampledProfile) { return traceFile; } + public double getTimestamp() { + return timestamp; + } + public @NotNull String getVersion() { return version; } @@ -152,20 +160,23 @@ public static final class Builder { private final @NotNull SentryId chunkId; private final @NotNull Map measurements; private final @NotNull File traceFile; + private final double timestamp; public Builder( final @NotNull SentryId profilerId, final @NotNull SentryId chunkId, final @NotNull Map measurements, - final @NotNull File traceFile) { + final @NotNull File traceFile, + final @NotNull SentryDate timestamp) { this.profilerId = profilerId; this.chunkId = chunkId; this.measurements = new ConcurrentHashMap<>(measurements); this.traceFile = traceFile; + this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); } public ProfileChunk build(SentryOptions options) { - return new ProfileChunk(profilerId, chunkId, traceFile, measurements, options); + return new ProfileChunk(profilerId, chunkId, traceFile, measurements, timestamp, options); } } @@ -182,6 +193,7 @@ public static final class JsonKeys { public static final String ENVIRONMENT = "environment"; public static final String VERSION = "version"; public static final String SAMPLED_PROFILE = "sampled_profile"; + public static final String TIMESTAMP = "timestamp"; } @Override @@ -208,6 +220,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (sampledProfile != null) { writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); } + writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -301,6 +314,12 @@ public static final class Deserializer implements JsonDeserializer data.sampledProfile = sampledProfile; } break; + case JsonKeys.TIMESTAMP: + Double timestamp = reader.nextDoubleOrNull(); + if (timestamp != null) { + data.timestamp = timestamp; + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 383ba50e84..85ea2bc224 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1051,11 +1051,13 @@ public static void endSession() { } /** Starts the continuous profiler, if enabled. */ + @ApiStatus.Experimental public static void startProfiler() { getCurrentScopes().startProfiler(); } - /** Starts the continuous profiler, if enabled. */ + /** Stops the continuous profiler, if enabled. */ + @ApiStatus.Experimental public static void stopProfiler() { getCurrentScopes().stopProfiler(); } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 8418d4cea4..ff42854431 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -357,15 +357,6 @@ public class SentryOptions { */ private @Nullable ProfilesSamplerCallback profilesSampler; - /** - * Configures the continuous profiling sample rate as a percentage of profiles to be sent in the - * range of 0.0 to 1.0. if 1.0 is set it means that 100% of profiles will be sent. If set to 0.1 - * only 10% of profiles will be sent. Profiles are picked randomly. Default is 1 (100%). - * ProfilesSampleRate takes precedence over this. To enable continuous profiling, don't set - * profilesSampleRate or profilesSampler, or set them to null. - */ - private double continuousProfilesSampleRate = 1.0; - /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; @@ -1713,6 +1704,7 @@ public void setTransactionProfiler(final @Nullable ITransactionProfiler transact * * @return the continuous profiler. */ + @ApiStatus.Experimental public @NotNull IContinuousProfiler getContinuousProfiler() { return continuousProfiler; } @@ -1722,6 +1714,7 @@ public void setTransactionProfiler(final @Nullable ITransactionProfiler transact * * @param continuousProfiler - the continuous profiler */ + @ApiStatus.Experimental 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() @@ -1749,7 +1742,7 @@ public boolean isProfilingEnabled() { public boolean isContinuousProfilingEnabled() { return profilesSampleRate == null && profilesSampler == null - && continuousProfilesSampleRate > 0; + && experimental.getContinuousProfilesSampleRate() > 0; } /** @@ -1803,18 +1796,9 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { * * @return the sample rate */ + @ApiStatus.Experimental public double getContinuousProfilesSampleRate() { - return continuousProfilesSampleRate; - } - - public void setContinuousProfilesSampleRate(final double continuousProfilesSampleRate) { - if (!SampleRateUtils.isValidContinuousProfilesSampleRate(continuousProfilesSampleRate)) { - throw new IllegalArgumentException( - "The value " - + continuousProfilesSampleRate - + " is not valid. Use values between 0.0 and 1.0."); - } - this.continuousProfilesSampleRate = continuousProfilesSampleRate; + return experimental.getContinuousProfilesSampleRate(); } /** @@ -2744,7 +2728,7 @@ public void merge(final @NotNull ExternalOptions options) { setProfilesSampleRate(options.getProfilesSampleRate()); } if (options.getContinuousProfilesSampleRate() != null) { - setContinuousProfilesSampleRate(options.getContinuousProfilesSampleRate()); + experimental.setContinuousProfilesSampleRate(options.getContinuousProfilesSampleRate()); } if (options.getDebug() != null) { setDebug(options.getDebug()); diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 7b8fc219b9..0b63116ee6 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -885,7 +885,7 @@ class JsonSerializerTest { 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 profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), 5.3, fixture.options) val measurementNow = SentryNanotimeDate() val measurementNowSeconds = BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN) @@ -928,6 +928,7 @@ class JsonSerializerTest { 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(5.3, element["timestamp"] as Double) assertEquals("sampled profile in base 64", element["sampled_profile"] as String) assertEquals( mapOf( @@ -992,6 +993,7 @@ class JsonSerializerTest { "profiler_id":"$profilerId", "release":"release", "sampled_profile":"sampled profile in base 64", + "timestamp":"5.3", "version":"2", "measurements":{ "screen_frame_rates": { @@ -1035,6 +1037,7 @@ class JsonSerializerTest { assertEquals(profilerId, profileChunk.profilerId) assertEquals("release", profileChunk.release) assertEquals("sampled profile in base 64", profileChunk.sampledProfile) + assertEquals(5.3, profileChunk.timestamp) assertEquals("2", profileChunk.version) val expectedMeasurements = mapOf( ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 107d9375a4..92343bdeb4 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2177,7 +2177,7 @@ class ScopesTest { val logger = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) - it.continuousProfilesSampleRate = 0.1 + it.experimental.continuousProfilesSampleRate = 0.1 it.setLogger(logger) it.isDebug = true } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index fc54dcb1bd..563e0725f0 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -98,7 +98,7 @@ 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) + profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), 1.0, sentryOptions) } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 03146f8180..4cea67c4ed 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -239,7 +239,7 @@ class SentryOptionsTest { @Test fun `when continuousProfilesSampleRate is set to a 0, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { - this.continuousProfilesSampleRate = 0.0 + this.experimental.continuousProfilesSampleRate = 0.0 } assertFalse(options.isProfilingEnabled) assertFalse(options.isContinuousProfilingEnabled) @@ -266,19 +266,19 @@ class SentryOptionsTest { @Test fun `when setContinuousProfilesSampleRate is set to exactly 0, value is set`() { val options = SentryOptions().apply { - this.continuousProfilesSampleRate = 0.0 + this.experimental.continuousProfilesSampleRate = 0.0 } assertEquals(0.0, options.continuousProfilesSampleRate) } @Test fun `when setContinuousProfilesSampleRate is set to higher than 1_0, setter throws`() { - assertFailsWith { SentryOptions().continuousProfilesSampleRate = 1.0000000000001 } + assertFailsWith { SentryOptions().experimental.continuousProfilesSampleRate = 1.0000000000001 } } @Test fun `when setContinuousProfilesSampleRate is set to lower than 0, setter throws`() { - assertFailsWith { SentryOptions().continuousProfilesSampleRate = -0.0000000000001 } + assertFailsWith { SentryOptions().experimental.continuousProfilesSampleRate = -0.0000000000001 } } @Test @@ -607,7 +607,7 @@ class SentryOptionsTest { fun `when profiling is disabled, isEnableAppStartProfiling is always false`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.continuousProfilesSampleRate = 0.0 + options.experimental.continuousProfilesSampleRate = 0.0 assertFalse(options.isEnableAppStartProfiling) } @@ -615,7 +615,7 @@ class SentryOptionsTest { fun `when setEnableAppStartProfiling is called and continuous profiling is enabled, isEnableAppStartProfiling is true`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.continuousProfilesSampleRate = 1.0 + options.experimental.continuousProfilesSampleRate = 1.0 assertTrue(options.isEnableAppStartProfiling) } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index ae0243618a..36dda68474 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -407,7 +407,7 @@ class SentryTest { var sentryOptions: SentryOptions? = null Sentry.init { it.dsn = dsn - it.continuousProfilesSampleRate = 1.0 + it.experimental.continuousProfilesSampleRate = 1.0 it.cacheDirPath = tempPath sentryOptions = it } @@ -422,7 +422,7 @@ class SentryTest { var sentryOptions: SentryOptions? = null Sentry.init { it.dsn = dsn - it.continuousProfilesSampleRate = 0.0 + it.experimental.continuousProfilesSampleRate = 0.0 it.cacheDirPath = tempPath sentryOptions = it } @@ -1336,7 +1336,7 @@ class SentryTest { Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) - it.continuousProfilesSampleRate = 0.1 + it.experimental.continuousProfilesSampleRate = 0.1 } // We cannot set sample rate to 0, as it would not start the profiler. So we set the seed to have consistent results SentryRandom.current().setSeed(0) diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 4523a6ecd1..2fd047c9ef 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -35,7 +35,7 @@ class TracesSamplerTest { options.profilesSampleRate = profilesSampleRate } if (continuousProfilesSampleRate != null) { - options.continuousProfilesSampleRate = continuousProfilesSampleRate + options.experimental.continuousProfilesSampleRate = continuousProfilesSampleRate } if (tracesSamplerCallback != null) { options.tracesSampler = tracesSamplerCallback From fcfa5763a736710ae47d8c73e9424188fa81c344 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 27 Feb 2025 17:00:36 +0100 Subject: [PATCH 08/14] Change continuous profiling to session sample rate (#4180) * Set continuousProfilesSampleRate and startProfiler() and stopProfiler() as experimental * Added chunk start timestamp to ProfileChunk * Moved setContinuousProfilesSampleRate into ExperimentalOptions * increased chunk duration to 1 minute * replaced continuousProfilesSampleRate with profileSessionSampleRate (Default null) * sample rate is now evaluated inside AndroidContinuousProfiler and every time a session finishes --- CHANGELOG.md | 6 +- .../api/sentry-android-core.api | 3 +- .../core/AndroidContinuousProfiler.java | 28 +++++- .../android/core/ManifestMetadataReader.java | 14 +-- .../core/SentryPerformanceProvider.java | 8 +- .../core/AndroidContinuousProfilerTest.kt | 98 +++++++++++++------ .../core/ManifestMetadataReaderTest.kt | 18 ++-- sentry/api/sentry.api | 18 ++-- .../java/io/sentry/ExperimentalOptions.java | 23 +++-- .../main/java/io/sentry/ExternalOptions.java | 11 --- .../java/io/sentry/IContinuousProfiler.java | 4 +- .../io/sentry/NoOpContinuousProfiler.java | 9 +- sentry/src/main/java/io/sentry/Scope.java | 4 + sentry/src/main/java/io/sentry/Scopes.java | 9 +- .../SentryAppStartProfilingOptions.java | 2 +- .../main/java/io/sentry/SentryOptions.java | 12 +-- .../main/java/io/sentry/TracesSampler.java | 6 +- .../java/io/sentry/util/SampleRateUtils.java | 4 +- .../java/io/sentry/ExternalOptionsTest.kt | 7 -- .../io/sentry/NoOpContinuousProfilerTest.kt | 8 +- sentry/src/test/java/io/sentry/ScopeTest.kt | 52 ++++++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 34 ++----- .../test/java/io/sentry/SentryOptionsTest.kt | 38 ++++--- sentry/src/test/java/io/sentry/SentryTest.kt | 25 ++--- .../test/java/io/sentry/TracesSamplerTest.kt | 24 ++--- .../java/io/sentry/util/SampleRateUtilTest.kt | 5 + 26 files changed, 283 insertions(+), 187 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721f319790..6535a66f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710)) - To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.continuousProfilesSampleRate` (defaults to 1.0). + To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.continuousProfilesSampleRate`, which defaults to null (disabled). Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling. ```java @@ -15,7 +15,7 @@ SentryAndroid.init(context) { options -> // Currently under experimental options: - options.getExperimental().setContinuousProfilesSampleRate(1.0); + options.getExperimental().setProfileSessionSampleRate(1.0); } // Start profiling Sentry.startProfiler(); @@ -29,7 +29,7 @@ SentryAndroid.init(context) { options -> // Currently under experimental options: - options.experimental.continuousProfilesSampleRate = 1.0 + options.experimental.profileSessionSampleRate = 1.0 } // Start profiling Sentry.startProfiler() diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b1b4535796..ad1fd69cf5 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -42,7 +42,8 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V - public fun start ()V + public fun reevaluateSampling ()V + public fun start (Lio/sentry/TracesSampler;)V public fun stop ()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 ac43ac53c3..764d29d1f9 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 @@ -20,6 +20,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryNanotimeDate; import io.sentry.SentryOptions; +import io.sentry.TracesSampler; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.SentryId; import io.sentry.transport.RateLimiter; @@ -55,6 +56,8 @@ public class AndroidContinuousProfiler private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); + private boolean shouldSample = true; + private boolean isSampled = false; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -100,7 +103,24 @@ private void init() { logger); } - public synchronized void start() { + public synchronized void start(final @NotNull TracesSampler tracesSampler) { + if (shouldSample) { + isSampled = tracesSampler.sampleSessionProfile(); + shouldSample = false; + } + if (!isSampled) { + logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); + return; + } + if (isRunning()) { + logger.log(SentryLevel.DEBUG, "Profiler is already running."); + return; + } + logger.log(SentryLevel.DEBUG, "Started Profiler."); + startProfile(); + } + + private synchronized void startProfile() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.getCurrentScopes(); @@ -236,7 +256,7 @@ private synchronized void stop(final boolean restartProfiler) { if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); - start(); + startProfile(); } else { // When the profiler is stopped manually, we have to reset its id profilerId = SentryId.EMPTY_ID; @@ -244,6 +264,10 @@ private synchronized void stop(final boolean restartProfiler) { } } + public synchronized void reevaluateSampling() { + shouldSample = true; + } + public synchronized void close() { stop(); isClosed.set(true); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 70085b2356..1d0fa44a9b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -64,8 +64,8 @@ final class ManifestMetadataReader { static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate"; - static final String CONTINUOUS_PROFILES_SAMPLE_RATE = - "io.sentry.traces.profiling.continuous-sample-rate"; + static final String PROFILE_SESSION_SAMPLE_RATE = + "io.sentry.traces.profiling.session-sample-rate"; @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; @@ -318,11 +318,11 @@ static void applyMetadata( } } - if (options.getContinuousProfilesSampleRate() == 1.0) { - final double continuousProfilesSampleRate = - readDouble(metadata, logger, CONTINUOUS_PROFILES_SAMPLE_RATE); - if (continuousProfilesSampleRate != -1) { - options.getExperimental().setContinuousProfilesSampleRate(continuousProfilesSampleRate); + if (options.getProfileSessionSampleRate() == null) { + final double profileSessionSampleRate = + readDouble(metadata, logger, PROFILE_SESSION_SAMPLE_RATE); + if (profileSessionSampleRate != -1) { + options.getExperimental().setProfileSessionSampleRate(profileSessionSampleRate); } } 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 583f984cc8..f7efa28654 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 @@ -22,6 +22,7 @@ import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.TracesSampler; import io.sentry.TracesSamplingDecision; import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; @@ -180,7 +181,12 @@ private void createAndStartContinuousProfiler( appStartMetrics.setAppStartProfiler(null); appStartMetrics.setAppStartContinuousProfiler(appStartContinuousProfiler); logger.log(SentryLevel.DEBUG, "App start continuous profiling started."); - appStartContinuousProfiler.start(); + SentryOptions sentryOptions = SentryOptions.empty(); + // Let's fake a sampler to accept the sampling decision that was calculated on last run + sentryOptions + .getExperimental() + .setProfileSessionSampleRate(profilingOptions.isContinuousProfileSampled() ? 1.0 : 0.0); + appStartContinuousProfiler.start(new TracesSampler(sentryOptions)); } private void createAndStartTransactionProfiler( 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 8093b2ee8b..79d151fe4c 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 @@ -17,6 +17,7 @@ import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryNanotimeDate import io.sentry.SentryTracer +import io.sentry.TracesSampler import io.sentry.TransactionContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector import io.sentry.profilemeasurements.ProfileMeasurement @@ -58,6 +59,7 @@ class AndroidContinuousProfilerTest { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) } val mockLogger = mock() + val mockTracesSampler = mock() val scopes: IScopes = mock() val frameMetricsCollector: SentryFrameMetricsCollector = mock() @@ -73,6 +75,10 @@ class AndroidContinuousProfilerTest { setLogger(mockLogger) } + init { + whenever(mockTracesSampler.sampleSessionProfile()).thenReturn(true) + } + fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { optionConfig(options) whenever(scopes.options).thenReturn(options) @@ -136,7 +142,7 @@ class AndroidContinuousProfilerTest { @Test fun `isRunning reflects profiler status`() { val profiler = fixture.getSut() - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) profiler.stop() assertFalse(profiler.isRunning) @@ -145,21 +151,57 @@ class AndroidContinuousProfilerTest { @Test fun `profiler multiple starts are ignored`() { val profiler = fixture.getSut() - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) - verify(fixture.mockLogger, never()).log(eq(SentryLevel.WARNING), eq("Profiling has already started...")) - profiler.start() - verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Profiling has already started...")) + verify(fixture.mockLogger, never()).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + profiler.start(fixture.mockTracesSampler) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) assertTrue(profiler.isRunning) } + @Test + fun `profiler logs a warning on start if not sampled`() { + val profiler = fixture.getSut() + whenever(fixture.mockTracesSampler.sampleSessionProfile()).thenReturn(false) + profiler.start(fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) + } + + @Test + fun `profiler evaluates sessionSampleRate only the first time`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile() + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.start(fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + // Then, the sessionSampleRate is not evaluated again + profiler.start(fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + } + + @Test + fun `when reevaluateSampling, profiler evaluates sessionSampleRate on next start`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile() + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.start(fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + // When reevaluateSampling is called, the sessionSampleRate is not evaluated immediately + profiler.reevaluateSampling() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + // Then, when the profiler starts again, the sessionSampleRate is reevaluated + profiler.start(fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile() + } + @Test fun `profiler works only on api 22+`() { val buildInfo = mock { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) } val profiler = fixture.getSut(buildInfo) - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -168,7 +210,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.profilesSampleRate = 0.0 } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) } @@ -184,8 +226,8 @@ class AndroidContinuousProfilerTest { ) // Regardless of how many times the profiler is started, the option is evaluated and logged only once - profiler.start() - profiler.start() + profiler.start(fixture.mockTracesSampler) + profiler.start(fixture.mockTracesSampler) verify(fixture.mockLogger, times(1)).log( SentryLevel.WARNING, "Disabling profiling because no profiling traces dir path is defined in options." @@ -205,8 +247,8 @@ class AndroidContinuousProfilerTest { ) // Regardless of how many times the profiler is started, the option is evaluated and logged only once - profiler.start() - profiler.start() + profiler.start(fixture.mockTracesSampler) + profiler.start(fixture.mockTracesSampler) verify(fixture.mockLogger, times(1)).log( SentryLevel.WARNING, "Disabling profiling because trace rate is set to %d", @@ -219,7 +261,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.cacheDirPath = null } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -228,7 +270,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.cacheDirPath = "" } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -237,7 +279,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.profilingTracesHz = 0 } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -248,7 +290,7 @@ class AndroidContinuousProfilerTest { it.executorService = mockExecutorService } whenever(mockExecutorService.submit(any>())).thenReturn(mock()) - profiler.start() + profiler.start(fixture.mockTracesSampler) verify(mockExecutorService, never()).submit(any()) profiler.stop() verify(mockExecutorService, never()).submit(any>()) @@ -259,7 +301,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { File(it.profilingTracesDirPath!!).setWritable(false) } - profiler.start() + profiler.start(fixture.mockTracesSampler) profiler.stop() // We assert that no trace files are written assertTrue( @@ -276,7 +318,7 @@ class AndroidContinuousProfilerTest { fixture.options.compositePerformanceCollector = performanceCollector val profiler = fixture.getSut() verify(performanceCollector, never()).start(any()) - profiler.start() + profiler.start(fixture.mockTracesSampler) verify(performanceCollector).start(any()) } @@ -285,7 +327,7 @@ class AndroidContinuousProfilerTest { val performanceCollector = mock() fixture.options.compositePerformanceCollector = performanceCollector val profiler = fixture.getSut() - profiler.start() + profiler.start(fixture.mockTracesSampler) verify(performanceCollector, never()).stop(any()) profiler.stop() verify(performanceCollector).stop(any()) @@ -296,7 +338,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut() val frameMetricsCollectorId = "id" whenever(fixture.frameMetricsCollector.startCollection(any())).thenReturn(frameMetricsCollectorId) - profiler.start() + profiler.start(fixture.mockTracesSampler) verify(fixture.frameMetricsCollector, never()).stopCollection(frameMetricsCollectorId) profiler.stop() verify(fixture.frameMetricsCollector).stopCollection(frameMetricsCollectorId) @@ -305,7 +347,7 @@ class AndroidContinuousProfilerTest { @Test fun `profiler stops profiling and clear scheduled job on close`() { val profiler = fixture.getSut() - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) profiler.close() @@ -327,7 +369,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) executorService.runAll() @@ -345,7 +387,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) executorService.runAll() @@ -369,7 +411,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start() + profiler.start(fixture.mockTracesSampler) profiler.stop() // We run the executor service to send the profile chunk executorService.runAll() @@ -388,7 +430,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) executorService.runAll() @@ -406,7 +448,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We close the profiler, which should prevent sending additional chunks @@ -426,7 +468,7 @@ class AndroidContinuousProfilerTest { val rateLimiter = mock() whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) - profiler.start() + profiler.start(fixture.mockTracesSampler) assertTrue(profiler.isRunning) // If the SDK is rate limited, the profiler should stop @@ -447,7 +489,7 @@ class AndroidContinuousProfilerTest { whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) // If the SDK is rate limited, the profiler should never start - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) assertEquals(SentryId.EMPTY_ID, profiler.profilerId) verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) @@ -464,7 +506,7 @@ class AndroidContinuousProfilerTest { } // If the device is offline, the profiler should never start - profiler.start() + profiler.start(fixture.mockTracesSampler) assertFalse(profiler.isRunning) assertEquals(SentryId.EMPTY_ID, profiler.profilerId) verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index e1f08b396e..fd736e5910 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -801,36 +801,36 @@ class ManifestMetadataReaderTest { } @Test - fun `applyMetadata reads continuousProfilesSampleRate from metadata`() { + fun `applyMetadata reads profileSessionSampleRate from metadata`() { // Arrange val expectedSampleRate = 0.99f - val bundle = bundleOf(ManifestMetadataReader.CONTINUOUS_PROFILES_SAMPLE_RATE to expectedSampleRate) + val bundle = bundleOf(ManifestMetadataReader.PROFILE_SESSION_SAMPLE_RATE to expectedSampleRate) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(expectedSampleRate.toDouble(), fixture.options.continuousProfilesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.profileSessionSampleRate) } @Test - fun `applyMetadata does not override continuousProfilesSampleRate from options`() { + fun `applyMetadata does not override profileSessionSampleRate from options`() { // Arrange val expectedSampleRate = 0.99f - fixture.options.experimental.continuousProfilesSampleRate = expectedSampleRate.toDouble() - val bundle = bundleOf(ManifestMetadataReader.CONTINUOUS_PROFILES_SAMPLE_RATE to 0.1f) + fixture.options.experimental.profileSessionSampleRate = expectedSampleRate.toDouble() + val bundle = bundleOf(ManifestMetadataReader.PROFILE_SESSION_SAMPLE_RATE to 0.1f) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(expectedSampleRate.toDouble(), fixture.options.continuousProfilesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.profileSessionSampleRate) } @Test - fun `applyMetadata without specifying continuousProfilesSampleRate, stays 1`() { + fun `applyMetadata without specifying profileSessionSampleRate, stays null`() { // Arrange val context = fixture.getContext() @@ -838,7 +838,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(1.0, fixture.options.continuousProfilesSampleRate) + assertNull(fixture.options.profileSessionSampleRate) } @Test diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4d93e217f8..6cb1fe0c51 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -443,9 +443,9 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (Z)V - public fun getContinuousProfilesSampleRate ()D + public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; - public fun setContinuousProfilesSampleRate (D)V + public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V } @@ -460,7 +460,6 @@ public final class io/sentry/ExternalOptions { public static fun from (Lio/sentry/config/PropertiesProvider;Lio/sentry/ILogger;)Lio/sentry/ExternalOptions; public fun getBundleIds ()Ljava/util/Set; public fun getContextTags ()Ljava/util/List; - public fun getContinuousProfilesSampleRate ()Ljava/lang/Double; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDebug ()Ljava/lang/Boolean; public fun getDist ()Ljava/lang/String; @@ -494,7 +493,6 @@ public final class io/sentry/ExternalOptions { public fun isGlobalHubMode ()Ljava/lang/Boolean; public fun isSendDefaultPii ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; - public fun setContinuousProfilesSampleRate (Ljava/lang/Double;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V @@ -726,7 +724,8 @@ 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 start ()V + public abstract fun reevaluateSampling ()V + public abstract fun start (Lio/sentry/TracesSampler;)V public abstract fun stop ()V } @@ -1409,7 +1408,8 @@ public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfi public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z - public fun start ()V + public fun reevaluateSampling ()V + public fun start (Lio/sentry/TracesSampler;)V public fun stop ()V } @@ -2953,7 +2953,6 @@ public class io/sentry/SentryOptions { public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; - public fun getContinuousProfilesSampleRate ()D public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; @@ -2995,6 +2994,7 @@ public class io/sentry/SentryOptions { public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; public fun getProfilingTracesDirPath ()Ljava/lang/String; @@ -3769,7 +3769,7 @@ public final class io/sentry/TraceContext$JsonKeys { public final class io/sentry/TracesSampler { public fun (Lio/sentry/SentryOptions;)V public fun sample (Lio/sentry/SamplingContext;)Lio/sentry/TracesSamplingDecision; - public fun sampleContinuousProfile ()Z + public fun sampleSessionProfile ()Z } public final class io/sentry/TracesSamplingDecision { @@ -6327,7 +6327,7 @@ public final class io/sentry/util/Random : java/io/Serializable { public final class io/sentry/util/SampleRateUtils { public fun ()V - public static fun isValidContinuousProfilesSampleRate (D)Z + public static fun isValidContinuousProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 1a473d7c46..7c8dd32853 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -3,6 +3,7 @@ import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Experimental options for new features, these options are going to be promoted to SentryOptions @@ -14,13 +15,11 @@ public final class ExperimentalOptions { private @NotNull SentryReplayOptions sessionReplay; /** - * Configures the continuous profiling sample rate as a percentage of profiles to be sent in the - * range of 0.0 to 1.0. if 1.0 is set it means that 100% of profiles will be sent. If set to 0.1 - * only 10% of profiles will be sent. Profiles are picked randomly. Default is 1 (100%). - * ProfilesSampleRate takes precedence over this. To enable continuous profiling, don't set - * profilesSampleRate or profilesSampler, or set them to null. + * Indicates the percentage in which the profiles for the session will be created. Specifying 0 + * means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null + * (disabled). */ - private double continuousProfilesSampleRate = 1.0; + private @Nullable Double profileSessionSampleRate; public ExperimentalOptions(final boolean empty) { this.sessionReplay = new SentryReplayOptions(empty); @@ -35,18 +34,18 @@ public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOpt this.sessionReplay = sessionReplayOptions; } - public double getContinuousProfilesSampleRate() { - return continuousProfilesSampleRate; + public @Nullable Double getProfileSessionSampleRate() { + return profileSessionSampleRate; } @ApiStatus.Experimental - public void setContinuousProfilesSampleRate(final double continuousProfilesSampleRate) { - if (!SampleRateUtils.isValidContinuousProfilesSampleRate(continuousProfilesSampleRate)) { + public void setProfileSessionSampleRate(final @Nullable Double profileSessionSampleRate) { + if (!SampleRateUtils.isValidContinuousProfilesSampleRate(profileSessionSampleRate)) { throw new IllegalArgumentException( "The value " - + continuousProfilesSampleRate + + profileSessionSampleRate + " is not valid. Use values between 0.0 and 1.0."); } - this.continuousProfilesSampleRate = continuousProfilesSampleRate; + this.profileSessionSampleRate = profileSessionSampleRate; } } diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 020b7aea9f..d9b075e1c8 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -28,7 +28,6 @@ public final class ExternalOptions { private @Nullable Boolean enableDeduplication; private @Nullable Double tracesSampleRate; private @Nullable Double profilesSampleRate; - private @Nullable Double continuousProfilesSampleRate; private @Nullable SentryOptions.RequestSize maxRequestBodySize; private final @NotNull Map tags = new ConcurrentHashMap<>(); private @Nullable SentryOptions.Proxy proxy; @@ -74,8 +73,6 @@ public final class ExternalOptions { propertiesProvider.getBooleanProperty("uncaught.handler.print-stacktrace")); options.setTracesSampleRate(propertiesProvider.getDoubleProperty("traces-sample-rate")); options.setProfilesSampleRate(propertiesProvider.getDoubleProperty("profiles-sample-rate")); - options.setContinuousProfilesSampleRate( - propertiesProvider.getDoubleProperty("continuous-profiles-sample-rate")); options.setDebug(propertiesProvider.getBooleanProperty("debug")); options.setEnableDeduplication(propertiesProvider.getBooleanProperty("enable-deduplication")); options.setSendClientReports(propertiesProvider.getBooleanProperty("send-client-reports")); @@ -287,14 +284,6 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { this.profilesSampleRate = profilesSampleRate; } - public @Nullable Double getContinuousProfilesSampleRate() { - return continuousProfilesSampleRate; - } - - public void setContinuousProfilesSampleRate(final @Nullable Double continuousProfilesSampleRate) { - this.continuousProfilesSampleRate = continuousProfilesSampleRate; - } - public @Nullable SentryOptions.RequestSize getMaxRequestBodySize() { return maxRequestBodySize; } diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java index 14ce41a815..bd37f6e14f 100644 --- a/sentry/src/main/java/io/sentry/IContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -9,13 +9,15 @@ public interface IContinuousProfiler { boolean isRunning(); - void start(); + void start(final @NotNull TracesSampler tracesSampler); void stop(); /** Cancel the profiler and stops it. Used on SDK close. */ void close(); + void reevaluateSampling(); + @NotNull SentryId getProfilerId(); } diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java index 4ccf7cc681..597c3c5cfb 100644 --- a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -13,9 +13,6 @@ public static NoOpContinuousProfiler getInstance() { return instance; } - @Override - public void start() {} - @Override public void stop() {} @@ -24,9 +21,15 @@ public boolean isRunning() { return false; } + @Override + public void start(final @NotNull TracesSampler tracesSampler) {} + @Override public void close() {} + @Override + public void reevaluateSampling() {} + @Override public @NotNull SentryId getProfilerId() { return SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 4dacc8e4da..93cfad4463 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -872,6 +872,8 @@ public SessionPair startSession() { if (session != null) { // Assumes session will NOT flush itself (Not passing any scopes to it) session.end(); + // Continuous profiler sample rate is reevaluated every time a session ends + options.getContinuousProfiler().reevaluateSampling(); } previousSession = session; @@ -945,6 +947,8 @@ public Session endSession() { try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { if (session != null) { session.end(); + // Continuous profiler sample rate is reevaluated every time a session ends + options.getContinuousProfiler().reevaluateSampling(); previousSession = session.clone(); session = null; } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 638127cc54..e2b47b2821 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -926,14 +926,7 @@ public void flush(long timeoutMillis) { @Override public void startProfiler() { if (getOptions().isContinuousProfilingEnabled()) { - if (getOptions().getInternalTracesSampler().sampleContinuousProfile()) { - getOptions().getLogger().log(SentryLevel.DEBUG, "Started continuous Profiling."); - getOptions().getContinuousProfiler().start(); - } else { - getOptions() - .getLogger() - .log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); - } + getOptions().getContinuousProfiler().start(getOptions().getInternalTracesSampler()); } else if (getOptions().isProfilingEnabled()) { getOptions() .getLogger() diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index c4d99a60f2..364a71e5ca 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -44,7 +44,7 @@ public SentryAppStartProfilingOptions() { traceSampleRate = samplingDecision.getSampleRate(); profileSampled = samplingDecision.getProfileSampled(); profileSampleRate = samplingDecision.getProfileSampleRate(); - continuousProfileSampled = options.getInternalTracesSampler().sampleContinuousProfile(); + continuousProfileSampled = options.getInternalTracesSampler().sampleSessionProfile(); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index ff42854431..02e34c68d4 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1742,7 +1742,8 @@ public boolean isProfilingEnabled() { public boolean isContinuousProfilingEnabled() { return profilesSampleRate == null && profilesSampler == null - && experimental.getContinuousProfilesSampleRate() > 0; + && experimental.getProfileSessionSampleRate() != null + && experimental.getProfileSessionSampleRate() > 0; } /** @@ -1790,15 +1791,15 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { } /** - * Returns the continuous profiling sample rate. Default is 1 (100%). ProfilesSampleRate takes + * Returns the session sample rate. Default is null (disabled). ProfilesSampleRate takes * precedence over this. To enable continuous profiling, don't set profilesSampleRate or * profilesSampler, or set them to null. * * @return the sample rate */ @ApiStatus.Experimental - public double getContinuousProfilesSampleRate() { - return experimental.getContinuousProfilesSampleRate(); + public @Nullable Double getProfileSessionSampleRate() { + return experimental.getProfileSessionSampleRate(); } /** @@ -2727,9 +2728,6 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getProfilesSampleRate() != null) { setProfilesSampleRate(options.getProfilesSampleRate()); } - if (options.getContinuousProfilesSampleRate() != null) { - experimental.setContinuousProfilesSampleRate(options.getContinuousProfilesSampleRate()); - } if (options.getDebug() != null) { setDebug(options.getDebug()); } diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index fcf6e3929a..59e9eef5ad 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -85,9 +85,9 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont return new TracesSamplingDecision(false, null, false, null); } - public boolean sampleContinuousProfile() { - final double sampling = options.getContinuousProfilesSampleRate(); - return sample(sampling); + public boolean sampleSessionProfile() { + final @Nullable Double sampling = options.getProfileSessionSampleRate(); + return sampling != null && sample(sampling); } private boolean sample(final @NotNull Double aDouble) { diff --git a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java index ab84cd0e9d..eb7f6da512 100644 --- a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java +++ b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java @@ -23,8 +23,8 @@ public static boolean isValidProfilesSampleRate(@Nullable Double profilesSampleR return isValidRate(profilesSampleRate, true); } - public static boolean isValidContinuousProfilesSampleRate(double profilesSampleRate) { - return isValidRate(profilesSampleRate, false); + public static boolean isValidContinuousProfilesSampleRate(@Nullable Double profilesSampleRate) { + return isValidRate(profilesSampleRate, true); } private static boolean isValidRate(final @Nullable Double rate, final boolean allowNull) { diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 3a1d66b264..b25f67405c 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -120,13 +120,6 @@ class ExternalOptionsTest { } } - @Test - fun `creates options with continuousProfilesSampleRate using external properties`() { - withPropertiesFile("continuous-profiles-sample-rate=0.2") { - assertEquals(0.2, it.continuousProfilesSampleRate) - } - } - @Test fun `creates options with enableDeduplication using external properties`() { withPropertiesFile("enable-deduplication=true") { diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt index e791651aef..30ab1d090a 100644 --- a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -1,6 +1,7 @@ package io.sentry import io.sentry.protocol.SentryId +import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -10,7 +11,7 @@ class NoOpContinuousProfilerTest { @Test fun `start does not throw`() = - profiler.start() + profiler.start(mock()) @Test fun `stop does not throw`() = @@ -29,4 +30,9 @@ class NoOpContinuousProfilerTest { fun `getProfilerId returns Empty SentryId`() { assertEquals(profiler.profilerId, SentryId.EMPTY_ID) } + + @Test + fun `reevaluateSampling does not throw`() { + profiler.reevaluateSampling() + } } diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index b8025735e8..cbd10a0741 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -10,6 +10,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.check import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -391,6 +392,57 @@ class ScopeTest { assertNull(session) } + @Test + fun `Starting a session multiple times reevaluates profileSessionSampleRate`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + // The first time a session is started, sample rate is not reevaluated, as there's no need + scope.startSession() + verify(profiler, never()).reevaluateSampling() + // The second time a session is started, sample rate is reevaluated + scope.startSession() + verify(profiler).reevaluateSampling() + // Every time a session is started with an already running one, sample rate is reevaluated + scope.startSession() + verify(profiler, times(2)).reevaluateSampling() + } + + @Test + fun `Scope ends a session and reevaluates profileSessionSampleRate`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + scope.startSession() + verify(profiler, never()).reevaluateSampling() + scope.endSession() + verify(profiler).reevaluateSampling() + } + + @Test + fun `Scope ends a session and does not reevaluate profileSessionSampleRate if none exist`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + scope.endSession() + verify(profiler, never()).reevaluateSampling() + } + @Test fun `withSession returns a callback with the current Session`() { val options = SentryOptions().apply { diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 92343bdeb4..e5674b3823 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -15,7 +15,6 @@ import io.sentry.test.callMethod import io.sentry.test.createSentryClientMock import io.sentry.test.createTestScopes import io.sentry.util.HintUtils -import io.sentry.util.SentryRandom import io.sentry.util.StringUtils import junit.framework.TestCase.assertSame import org.mockito.kotlin.any @@ -1805,6 +1804,7 @@ class ScopesTest { setTransactionProfiler(profiler) compositePerformanceCollector = performanceCollector setContinuousProfiler(continuousProfiler) + experimental.profileSessionSampleRate = 1.0 } val sut = createScopes(options) sut.close() @@ -2166,27 +2166,26 @@ class ScopesTest { val profiler = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 } scopes.startProfiler() - verify(profiler).start() + verify(profiler).start(any()) } @Test - fun `startProfiler logs a message if not sampled`() { + fun `startProfiler logs instructions if continuous profiling is disabled`() { val profiler = mock() val logger = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) - it.experimental.continuousProfilesSampleRate = 0.1 + it.experimental.profileSessionSampleRate = 1.0 + it.profilesSampleRate = 1.0 it.setLogger(logger) it.isDebug = true } - // We cannot set sample rate to 0, as it would not start the profiler. So we set the seed to have consistent results - SentryRandom.current().setSeed(0) scopes.startProfiler() - - verify(profiler, never()).start() - verify(logger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) + verify(profiler, never()).start(any()) + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) } @Test @@ -2194,32 +2193,19 @@ class ScopesTest { val profiler = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 } 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.experimental.profileSessionSampleRate = 1.0 it.profilesSampleRate = 1.0 it.setLogger(logger) it.isDebug = true diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 4cea67c4ed..721d4c3fa2 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -195,17 +195,17 @@ class SentryOptionsTest { @Test fun `when options is initialized, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { assertFalse(SentryOptions().isProfilingEnabled) - assertTrue(SentryOptions().isContinuousProfilingEnabled) + assertFalse(SentryOptions().isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { + fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampleRate = null this.profilesSampler = null } assertFalse(options.isProfilingEnabled) - assertTrue(options.isContinuousProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test @@ -237,14 +237,22 @@ class SentryOptionsTest { } @Test - fun `when continuousProfilesSampleRate is set to a 0, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { + fun `when profileSessionSampleRate is set to 0, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { - this.experimental.continuousProfilesSampleRate = 0.0 + this.experimental.profileSessionSampleRate = 0.0 } assertFalse(options.isProfilingEnabled) assertFalse(options.isContinuousProfilingEnabled) } + @Test + fun `when profileSessionSampleRate is null, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { + val options = SentryOptions() + assertNull(options.experimental.profileSessionSampleRate) + assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) + } + @Test fun `when setProfilesSampleRate is set to exactly 0, value is set`() { val options = SentryOptions().apply { @@ -264,21 +272,21 @@ class SentryOptionsTest { } @Test - fun `when setContinuousProfilesSampleRate is set to exactly 0, value is set`() { + fun `when profileSessionSampleRate is set to exactly 0, value is set`() { val options = SentryOptions().apply { - this.experimental.continuousProfilesSampleRate = 0.0 + this.experimental.profileSessionSampleRate = 0.0 } - assertEquals(0.0, options.continuousProfilesSampleRate) + assertEquals(0.0, options.profileSessionSampleRate) } @Test - fun `when setContinuousProfilesSampleRate is set to higher than 1_0, setter throws`() { - assertFailsWith { SentryOptions().experimental.continuousProfilesSampleRate = 1.0000000000001 } + fun `when profileSessionSampleRate is set to higher than 1_0, setter throws`() { + assertFailsWith { SentryOptions().experimental.profileSessionSampleRate = 1.0000000000001 } } @Test - fun `when setContinuousProfilesSampleRate is set to lower than 0, setter throws`() { - assertFailsWith { SentryOptions().experimental.continuousProfilesSampleRate = -0.0000000000001 } + fun `when profileSessionSampleRate is set to lower than 0, setter throws`() { + assertFailsWith { SentryOptions().experimental.profileSessionSampleRate = -0.0000000000001 } } @Test @@ -349,7 +357,6 @@ class SentryOptionsTest { externalOptions.enableUncaughtExceptionHandler = false externalOptions.tracesSampleRate = 0.5 externalOptions.profilesSampleRate = 0.5 - externalOptions.continuousProfilesSampleRate = 0.3 externalOptions.addInAppInclude("com.app") externalOptions.addInAppExclude("io.off") externalOptions.addTracePropagationTarget("localhost") @@ -396,7 +403,6 @@ class SentryOptionsTest { assertFalse(options.isEnableUncaughtExceptionHandler) assertEquals(0.5, options.tracesSampleRate) assertEquals(0.5, options.profilesSampleRate) - assertEquals(0.3, options.continuousProfilesSampleRate) assertEquals(listOf("com.app"), options.inAppIncludes) assertEquals(listOf("io.off"), options.inAppExcludes) assertEquals(listOf("localhost", "api.foo.com"), options.tracePropagationTargets) @@ -607,7 +613,7 @@ class SentryOptionsTest { fun `when profiling is disabled, isEnableAppStartProfiling is always false`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.experimental.continuousProfilesSampleRate = 0.0 + options.experimental.profileSessionSampleRate = 0.0 assertFalse(options.isEnableAppStartProfiling) } @@ -615,7 +621,7 @@ class SentryOptionsTest { fun `when setEnableAppStartProfiling is called and continuous profiling is enabled, isEnableAppStartProfiling is true`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.experimental.continuousProfilesSampleRate = 1.0 + options.experimental.profileSessionSampleRate = 1.0 assertTrue(options.isEnableAppStartProfiling) } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 36dda68474..1a9b4f4f1e 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -18,7 +18,6 @@ import io.sentry.test.ImmediateExecutorService import io.sentry.test.createSentryClientMock import io.sentry.test.injectForField import io.sentry.util.PlatformTestManipulator -import io.sentry.util.SentryRandom import io.sentry.util.thread.IThreadChecker import io.sentry.util.thread.ThreadChecker import org.awaitility.kotlin.await @@ -407,7 +406,7 @@ class SentryTest { var sentryOptions: SentryOptions? = null Sentry.init { it.dsn = dsn - it.experimental.continuousProfilesSampleRate = 1.0 + it.experimental.profileSessionSampleRate = 1.0 it.cacheDirPath = tempPath sentryOptions = it } @@ -422,7 +421,7 @@ class SentryTest { var sentryOptions: SentryOptions? = null Sentry.init { it.dsn = dsn - it.experimental.continuousProfilesSampleRate = 0.0 + it.experimental.profileSessionSampleRate = 0.0 it.cacheDirPath = tempPath sentryOptions = it } @@ -1313,9 +1312,10 @@ class SentryTest { Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 } Sentry.startProfiler() - verify(profiler).start() + verify(profiler).start(any()) } @Test @@ -1327,21 +1327,7 @@ class SentryTest { it.profilesSampleRate = 1.0 } Sentry.startProfiler() - verify(profiler, never()).start() - } - - @Test - fun `startProfiler is ignored when not sampled`() { - val profiler = mock() - Sentry.init { - it.dsn = dsn - it.setContinuousProfiler(profiler) - it.experimental.continuousProfilesSampleRate = 0.1 - } - // We cannot set sample rate to 0, as it would not start the profiler. So we set the seed to have consistent results - SentryRandom.current().setSeed(0) - Sentry.startProfiler() - verify(profiler, never()).start() + verify(profiler, never()).start(any()) } @Test @@ -1350,6 +1336,7 @@ class SentryTest { Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 } Sentry.stopProfiler() verify(profiler).stop() diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 2fd047c9ef..46bec1077e 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -18,7 +18,7 @@ class TracesSamplerTest { randomResult: Double? = null, tracesSampleRate: Double? = null, profilesSampleRate: Double? = null, - continuousProfilesSampleRate: Double? = null, + profileSessionSampleRate: Double? = null, tracesSamplerCallback: SentryOptions.TracesSamplerCallback? = null, profilesSamplerCallback: SentryOptions.ProfilesSamplerCallback? = null, logger: ILogger? = null @@ -34,8 +34,8 @@ class TracesSamplerTest { if (profilesSampleRate != null) { options.profilesSampleRate = profilesSampleRate } - if (continuousProfilesSampleRate != null) { - options.experimental.continuousProfilesSampleRate = continuousProfilesSampleRate + if (profileSessionSampleRate != null) { + options.experimental.profileSessionSampleRate = profileSessionSampleRate } if (tracesSamplerCallback != null) { options.tracesSampler = tracesSamplerCallback @@ -155,23 +155,23 @@ class TracesSamplerTest { } @Test - fun `when continuousProfilesSampleRate is not set returns true`() { + fun `when profileSessionSampleRate is not set returns true`() { val sampler = fixture.getSut(randomResult = 1.0) - val sampled = sampler.sampleContinuousProfile() - assertTrue(sampled) + val sampled = sampler.sampleSessionProfile() + assertFalse(sampled) } @Test - fun `when continuousProfilesSampleRate is set and random returns lower number returns true`() { - val sampler = fixture.getSut(randomResult = 0.1, continuousProfilesSampleRate = 0.2) - val sampled = sampler.sampleContinuousProfile() + fun `when profileSessionSampleRate is set and random returns lower number returns true`() { + val sampler = fixture.getSut(randomResult = 0.1, profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile() assertTrue(sampled) } @Test - fun `when continuousProfilesSampleRate is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, continuousProfilesSampleRate = 0.2) - val sampled = sampler.sampleContinuousProfile() + fun `when profileSessionSampleRate is set and random returns greater number returns false`() { + val sampler = fixture.getSut(randomResult = 0.9, profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile() assertFalse(sampled) } diff --git a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt index 3a14f2bec5..5a8400dad0 100644 --- a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt +++ b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt @@ -141,6 +141,11 @@ class SampleRateUtilTest { assertTrue(SampleRateUtils.isValidContinuousProfilesSampleRate(1.0)) } + @Test + fun `accepts null continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidProfilesSampleRate(null)) + } + @Test fun `rejects negative continuous profiles sample rate`() { assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(-0.5)) From 1440de9042da493dc151a0f8072ec2f66dd26834 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 27 Feb 2025 17:14:51 +0100 Subject: [PATCH 09/14] Rename continuous profiling APIs (#4182) * renamed Sentry.startProfiler with Sentry.startProfileSession and Sentry.stopProfiler with Sentry.stopProfileSession --- CHANGELOG.md | 10 +++--- .../sentry/samples/android/MyApplication.java | 2 +- sentry/api/sentry.api | 32 +++++++++---------- .../src/main/java/io/sentry/HubAdapter.java | 8 ++--- .../main/java/io/sentry/HubScopesWrapper.java | 8 ++--- sentry/src/main/java/io/sentry/IScopes.java | 4 +-- sentry/src/main/java/io/sentry/NoOpHub.java | 4 +-- .../src/main/java/io/sentry/NoOpScopes.java | 4 +-- sentry/src/main/java/io/sentry/Scopes.java | 4 +-- .../main/java/io/sentry/ScopesAdapter.java | 8 ++--- sentry/src/main/java/io/sentry/Sentry.java | 8 ++--- .../src/test/java/io/sentry/HubAdapterTest.kt | 12 +++---- sentry/src/test/java/io/sentry/NoOpHubTest.kt | 4 +-- .../test/java/io/sentry/ScopesAdapterTest.kt | 12 +++---- sentry/src/test/java/io/sentry/ScopesTest.kt | 16 +++++----- sentry/src/test/java/io/sentry/SentryTest.kt | 16 +++++----- 16 files changed, 76 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6535a66f11..a92785e43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710)) - To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.continuousProfilesSampleRate`, which defaults to null (disabled). + To enable Continuous Profiling use the `Sentry.startProfileSession` and `Sentry.stopProfileSession` experimental APIs. Sampling rate can be set through `options.profileSessionSampleRate`, which defaults to null (disabled). Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling. ```java @@ -18,10 +18,10 @@ options.getExperimental().setProfileSessionSampleRate(1.0); } // Start profiling - Sentry.startProfiler(); + Sentry.startProfileSession(); // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. - Sentry.stopProfiler(); + Sentry.stopProfileSession(); ``` ```kotlin import io.sentry.android.core.SentryAndroid @@ -32,10 +32,10 @@ options.experimental.profileSessionSampleRate = 1.0 } // Start profiling - Sentry.startProfiler() + Sentry.startProfileSession() // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. - Sentry.stopProfiler() + Sentry.stopProfileSession() ``` To learn more visit [Sentry's Continuous Profiling](https://docs.sentry.io/product/explore/profiling/transaction-vs-continuous-profiling/#continuous-profiling-mode) documentation page. 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 572c4cdba7..13117e39e6 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 @@ -9,7 +9,7 @@ public class MyApplication extends Application { @Override public void onCreate() { - Sentry.startProfiler(); + Sentry.startProfileSession(); strictMode(); super.onCreate(); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6cb1fe0c51..598631725d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -625,10 +625,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -692,10 +692,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -931,13 +931,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 startProfileSession ()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 stopProfileSession ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1478,10 +1478,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1640,10 +1640,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2298,10 +2298,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2365,10 +2365,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun stopProfiler ()V + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2471,14 +2471,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 startProfileSession ()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 stopProfileSession ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 537bcdf104..dadb40eff5 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -278,13 +278,13 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { } @Override - public void startProfiler() { - Sentry.startProfiler(); + public void startProfileSession() { + Sentry.startProfileSession(); } @Override - public void stopProfiler() { - Sentry.stopProfiler(); + public void stopProfileSession() { + Sentry.stopProfileSession(); } @Override diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index d6755c2c41..1b3aa40843 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -278,13 +278,13 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { } @Override - public void startProfiler() { - scopes.startProfiler(); + public void startProfileSession() { + scopes.startProfileSession(); } @Override - public void stopProfiler() { - scopes.stopProfiler(); + public void stopProfileSession() { + scopes.stopProfileSession(); } @ApiStatus.Internal diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index 59a577f04b..3b2bbef34d 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -592,9 +592,9 @@ ITransaction startTransaction( final @NotNull TransactionContext transactionContext, final @NotNull TransactionOptions transactionOptions); - void startProfiler(); + void startProfileSession(); - void stopProfiler(); + void stopProfileSession(); /** * Associates {@link ISpan} and the transaction name with the {@link Throwable}. Used to determine diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 925f1e64ee..71f5d3fb78 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -244,10 +244,10 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { } @Override - public void startProfiler() {} + public void startProfileSession() {} @Override - public void stopProfiler() {} + public void stopProfileSession() {} @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 11bae042b0..25d2037cca 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -239,10 +239,10 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { } @Override - public void startProfiler() {} + public void startProfileSession() {} @Override - public void stopProfiler() {} + public void stopProfileSession() {} @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index e2b47b2821..c57a86b264 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -924,7 +924,7 @@ public void flush(long timeoutMillis) { } @Override - public void startProfiler() { + public void startProfileSession() { if (getOptions().isContinuousProfilingEnabled()) { getOptions().getContinuousProfiler().start(getOptions().getInternalTracesSampler()); } else if (getOptions().isProfilingEnabled()) { @@ -937,7 +937,7 @@ public void startProfiler() { } @Override - public void stopProfiler() { + public void stopProfileSession() { if (getOptions().isContinuousProfilingEnabled()) { getOptions().getLogger().log(SentryLevel.DEBUG, "Stopped continuous Profiling."); getOptions().getContinuousProfiler().stop(); diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 9944a87a0a..62ba91993c 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -281,13 +281,13 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { } @Override - public void startProfiler() { - Sentry.startProfiler(); + public void startProfileSession() { + Sentry.startProfileSession(); } @Override - public void stopProfiler() { - Sentry.stopProfiler(); + public void stopProfileSession() { + Sentry.stopProfileSession(); } @ApiStatus.Internal diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 85ea2bc224..02e7d168c7 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1052,14 +1052,14 @@ public static void endSession() { /** Starts the continuous profiler, if enabled. */ @ApiStatus.Experimental - public static void startProfiler() { - getCurrentScopes().startProfiler(); + public static void startProfileSession() { + getCurrentScopes().startProfileSession(); } /** Stops the continuous profiler, if enabled. */ @ApiStatus.Experimental - public static void stopProfiler() { - getCurrentScopes().stopProfiler(); + public static void stopProfileSession() { + getCurrentScopes().stopProfileSession(); } /** diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 310c153709..06b9870a03 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -266,13 +266,13 @@ class HubAdapterTest { verify(scopes).reportFullyDisplayed() } - @Test fun `startProfiler calls Hub`() { - HubAdapter.getInstance().startProfiler() - verify(scopes).startProfiler() + @Test fun `startProfileSession calls Hub`() { + HubAdapter.getInstance().startProfileSession() + verify(scopes).startProfileSession() } - @Test fun `stopProfiler calls Hub`() { - HubAdapter.getInstance().stopProfiler() - verify(scopes).stopProfiler() + @Test fun `stopProfileSession calls Hub`() { + HubAdapter.getInstance().stopProfileSession() + verify(scopes).stopProfileSession() } } diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index e0eb08ded0..fdf6185970 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -117,8 +117,8 @@ class NoOpHubTest { } @Test - fun `startProfiler doesnt throw`() = sut.startProfiler() + fun `startProfileSession doesnt throw`() = sut.startProfileSession() @Test - fun `stopProfiler doesnt throw`() = sut.stopProfiler() + fun `stopProfileSession doesnt throw`() = sut.stopProfileSession() } diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index 9c7418c6b5..35645a8986 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -266,13 +266,13 @@ class ScopesAdapterTest { verify(scopes).reportFullyDisplayed() } - @Test fun `startProfiler calls Scopes`() { - ScopesAdapter.getInstance().startProfiler() - verify(scopes).startProfiler() + @Test fun `startProfileSession calls Scopes`() { + ScopesAdapter.getInstance().startProfileSession() + verify(scopes).startProfileSession() } - @Test fun `stopProfiler calls Scopes`() { - ScopesAdapter.getInstance().stopProfiler() - verify(scopes).stopProfiler() + @Test fun `stopProfileSession calls Scopes`() { + ScopesAdapter.getInstance().stopProfileSession() + verify(scopes).stopProfileSession() } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index e5674b3823..9cb8ca177a 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2162,18 +2162,18 @@ class ScopesTest { } @Test - fun `startProfiler starts the continuous profiler`() { + fun `startProfileSession starts the continuous profiler`() { val profiler = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) it.experimental.profileSessionSampleRate = 1.0 } - scopes.startProfiler() + scopes.startProfileSession() verify(profiler).start(any()) } @Test - fun `startProfiler logs instructions if continuous profiling is disabled`() { + fun `startProfileSession logs instructions if continuous profiling is disabled`() { val profiler = mock() val logger = mock() val scopes = generateScopes { @@ -2183,24 +2183,24 @@ class ScopesTest { it.setLogger(logger) it.isDebug = true } - scopes.startProfiler() + scopes.startProfileSession() verify(profiler, never()).start(any()) verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) } @Test - fun `stopProfiler stops the continuous profiler`() { + fun `stopProfileSession stops the continuous profiler`() { val profiler = mock() val scopes = generateScopes { it.setContinuousProfiler(profiler) it.experimental.profileSessionSampleRate = 1.0 } - scopes.stopProfiler() + scopes.stopProfileSession() verify(profiler).stop() } @Test - fun `stopProfiler logs instructions if continuous profiling is disabled`() { + fun `stopProfileSession logs instructions if continuous profiling is disabled`() { val profiler = mock() val logger = mock() val scopes = generateScopes { @@ -2210,7 +2210,7 @@ class ScopesTest { it.setLogger(logger) it.isDebug = true } - scopes.stopProfiler() + scopes.stopProfileSession() 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.")) } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 1a9b4f4f1e..ef337dff90 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1307,50 +1307,50 @@ class SentryTest { } @Test - fun `startProfiler starts the continuous profiler`() { + fun `startProfileSession starts the continuous profiler`() { val profiler = mock() Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) it.experimental.profileSessionSampleRate = 1.0 } - Sentry.startProfiler() + Sentry.startProfileSession() verify(profiler).start(any()) } @Test - fun `startProfiler is ignored when continuous profiling is disabled`() { + fun `startProfileSession is ignored when continuous profiling is disabled`() { val profiler = mock() Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) it.profilesSampleRate = 1.0 } - Sentry.startProfiler() + Sentry.startProfileSession() verify(profiler, never()).start(any()) } @Test - fun `stopProfiler stops the continuous profiler`() { + fun `stopProfileSession stops the continuous profiler`() { val profiler = mock() Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) it.experimental.profileSessionSampleRate = 1.0 } - Sentry.stopProfiler() + Sentry.stopProfileSession() verify(profiler).stop() } @Test - fun `stopProfiler is ignored when continuous profiling is disabled`() { + fun `stopProfileSession is ignored when continuous profiling is disabled`() { val profiler = mock() Sentry.init { it.dsn = dsn it.setContinuousProfiler(profiler) it.profilesSampleRate = 1.0 } - Sentry.stopProfiler() + Sentry.stopProfileSession() verify(profiler, never()).stop() } From 27d91aa2d591c38382e52676ecc36829154463d8 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 14 Mar 2025 17:17:43 +0100 Subject: [PATCH 10/14] Add continuous profiling ProfileLifecycle (#4202) * Added ProfileLifecycle * Sentry.startProfileSession() will work only in MANUAL mode * Tracer start will start profiler only in TRACE mode * Tracer and spans now attach profilerId only when sampled --- .../api/sentry-android-core.api | 5 +- .../core/AndroidContinuousProfiler.java | 75 ++++++++++--- .../android/core/ManifestMetadataReader.java | 16 +++ .../core/SentryPerformanceProvider.java | 3 +- .../core/AndroidContinuousProfilerTest.kt | 105 +++++++++++------- .../core/ManifestMetadataReaderTest.kt | 27 +++++ sentry/api/sentry.api | 21 +++- .../java/io/sentry/ExperimentalOptions.java | 26 +++++ .../java/io/sentry/IContinuousProfiler.java | 5 +- .../io/sentry/NoOpContinuousProfiler.java | 6 +- .../main/java/io/sentry/ProfileLifecycle.java | 18 +++ sentry/src/main/java/io/sentry/Scopes.java | 54 +++++++-- .../SentryAppStartProfilingOptions.java | 25 +++++ .../main/java/io/sentry/SentryOptions.java | 11 ++ .../src/main/java/io/sentry/SentryTracer.java | 8 +- .../test/java/io/sentry/JsonSerializerTest.kt | 8 +- .../io/sentry/NoOpContinuousProfilerTest.kt | 4 +- sentry/src/test/java/io/sentry/ScopesTest.kt | 90 ++++++++++++++- .../test/java/io/sentry/SentryOptionsTest.kt | 14 +++ sentry/src/test/java/io/sentry/SentryTest.kt | 29 ++++- .../test/java/io/sentry/SentryTracerTest.kt | 53 ++++++++- 21 files changed, 509 insertions(+), 94 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/ProfileLifecycle.java diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index ad1fd69cf5..fe65fab824 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -40,11 +40,12 @@ public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IConti 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 getRootSpanCounter ()I public fun isRunning ()Z public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V public fun reevaluateSampling ()V - public fun start (Lio/sentry/TracesSampler;)V - public fun stop ()V + public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V } public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector { 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 764d29d1f9..3f840f2179 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,7 +4,6 @@ import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; import static java.util.concurrent.TimeUnit.SECONDS; -import android.annotation.SuppressLint; import android.os.Build; import io.sentry.CompositePerformanceCollector; import io.sentry.DataCategory; @@ -15,6 +14,7 @@ import io.sentry.NoOpScopes; import io.sentry.PerformanceCollectionData; import io.sentry.ProfileChunk; +import io.sentry.ProfileLifecycle; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryLevel; @@ -58,6 +58,7 @@ public class AndroidContinuousProfiler private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); private boolean shouldSample = true; private boolean isSampled = false; + private int rootSpanCounter = 0; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -103,7 +104,10 @@ private void init() { logger); } - public synchronized void start(final @NotNull TracesSampler tracesSampler) { + @Override + public synchronized void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) { if (shouldSample) { isSampled = tracesSampler.sampleSessionProfile(); shouldSample = false; @@ -112,15 +116,31 @@ public synchronized void start(final @NotNull TracesSampler tracesSampler) { logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); return; } - if (isRunning()) { - logger.log(SentryLevel.DEBUG, "Profiler is already running."); - return; + switch (profileLifecycle) { + case TRACE: + // rootSpanCounter should never be negative, unless the user changed profile lifecycle while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + rootSpanCounter++; + break; + case MANUAL: + // We check if the profiler is already running and log a message only in manual mode, since + // in trace mode we can have multiple concurrent traces + if (isRunning()) { + logger.log(SentryLevel.DEBUG, "Profiler is already running."); + return; + } + break; + } + if (!isRunning()) { + logger.log(SentryLevel.DEBUG, "Started Profiler."); + start(); } - logger.log(SentryLevel.DEBUG, "Started Profiler."); - startProfile(); } - private synchronized void startProfile() { + private synchronized void start() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.getCurrentScopes(); @@ -150,7 +170,7 @@ private synchronized void startProfile() { || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); // Let's stop and reset profiler id, as the profile is now broken anyway - stop(); + stop(false); return; } @@ -158,7 +178,7 @@ private synchronized void startProfile() { if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) { logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler."); // Let's stop and reset profiler id, as the profile is now broken anyway - stop(); + stop(false); return; } startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now(); @@ -195,11 +215,28 @@ private synchronized void startProfile() { } } - public synchronized void stop() { - stop(false); + @Override + public synchronized void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) { + switch (profileLifecycle) { + case TRACE: + rootSpanCounter--; + // If there are active spans, and profile lifecycle is trace, we don't stop the profiler + if (rootSpanCounter > 0) { + return; + } + // rootSpanCounter should never be negative, unless the user changed profile lifecycle while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + stop(false); + break; + case MANUAL: + stop(false); + break; + } } - @SuppressLint("NewApi") private synchronized void stop(final boolean restartProfiler) { if (stopFuture != null) { stopFuture.cancel(true); @@ -256,7 +293,7 @@ private synchronized void stop(final boolean restartProfiler) { if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); - startProfile(); + start(); } else { // When the profiler is stopped manually, we have to reset its id profilerId = SentryId.EMPTY_ID; @@ -269,7 +306,8 @@ public synchronized void reevaluateSampling() { } public synchronized void close() { - stop(); + rootSpanCounter = 0; + stop(false); isClosed.set(true); } @@ -315,13 +353,18 @@ Future getStopFuture() { return stopFuture; } + @VisibleForTesting + public int getRootSpanCounter() { + return rootSpanCounter; + } + @Override public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { // We stop the profiler as soon as we are rate limited, to avoid the performance overhead if (rateLimiter.isActiveForCategory(All) || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); - stop(); + stop(false); } // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's // useless to restart it automatically diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 1d0fa44a9b..e85e5de923 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -6,6 +6,7 @@ import android.os.Bundle; import io.sentry.ILogger; import io.sentry.InitPriority; +import io.sentry.ProfileLifecycle; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SdkVersion; @@ -67,6 +68,8 @@ final class ManifestMetadataReader { static final String PROFILE_SESSION_SAMPLE_RATE = "io.sentry.traces.profiling.session-sample-rate"; + static final String PROFILE_LIFECYCLE = "io.sentry.traces.profiling.lifecycle"; + @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; @@ -326,6 +329,19 @@ static void applyMetadata( } } + final String profileLifecycle = + readString( + metadata, + logger, + PROFILE_LIFECYCLE, + options.getProfileLifecycle().name().toLowerCase(Locale.ROOT)); + if (profileLifecycle != null) { + options + .getExperimental() + .setProfileLifecycle( + ProfileLifecycle.valueOf(profileLifecycle.toUpperCase(Locale.ROOT))); + } + options.setEnableUserInteractionTracing( readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); 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 f7efa28654..df05f880d8 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 @@ -186,7 +186,8 @@ private void createAndStartContinuousProfiler( sentryOptions .getExperimental() .setProfileSessionSampleRate(profilingOptions.isContinuousProfileSampled() ? 1.0 : 0.0); - appStartContinuousProfiler.start(new TracesSampler(sentryOptions)); + appStartContinuousProfiler.startProfileSession( + profilingOptions.getProfileLifecycle(), new TracesSampler(sentryOptions)); } private void createAndStartTransactionProfiler( 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 79d151fe4c..e77d82fe46 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 @@ -13,6 +13,7 @@ import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData +import io.sentry.ProfileLifecycle import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryNanotimeDate @@ -142,28 +143,54 @@ class AndroidContinuousProfilerTest { @Test fun `isRunning reflects profiler status`() { val profiler = fixture.getSut() - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) - profiler.stop() + profiler.stopProfileSession(ProfileLifecycle.MANUAL) assertFalse(profiler.isRunning) } @Test - fun `profiler multiple starts are ignored`() { + fun `profiler multiple starts are ignored in manual mode`() { val profiler = fixture.getSut() - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) verify(fixture.mockLogger, never()).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) assertTrue(profiler.isRunning) + assertEquals(0, profiler.rootSpanCounter) + } + + @Test + fun `profiler multiple starts are accepted in trace mode`() { + val profiler = fixture.getSut() + + // rootSpanCounter is incremented when the profiler starts in trace mode + assertEquals(0, profiler.rootSpanCounter) + profiler.startProfileSession(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + profiler.startProfileSession(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + verify(fixture.mockLogger, never()).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + assertTrue(profiler.isRunning) + assertEquals(2, profiler.rootSpanCounter) + + // rootSpanCounter is decremented when the profiler stops in trace mode, and keeps running until rootSpanCounter is 0 + profiler.stopProfileSession(ProfileLifecycle.TRACE) + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + + // only when rootSpanCounter is 0 the profiler stops + profiler.stopProfileSession(ProfileLifecycle.TRACE) + assertEquals(0, profiler.rootSpanCounter) + assertFalse(profiler.isRunning) } @Test fun `profiler logs a warning on start if not sampled`() { val profiler = fixture.getSut() whenever(fixture.mockTracesSampler.sampleSessionProfile()).thenReturn(false) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) } @@ -173,10 +200,10 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut() verify(fixture.mockTracesSampler, never()).sampleSessionProfile() // The first time the profiler is started, the sessionSampleRate is evaluated - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() // Then, the sessionSampleRate is not evaluated again - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() } @@ -185,13 +212,13 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut() verify(fixture.mockTracesSampler, never()).sampleSessionProfile() // The first time the profiler is started, the sessionSampleRate is evaluated - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() // When reevaluateSampling is called, the sessionSampleRate is not evaluated immediately profiler.reevaluateSampling() verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() // Then, when the profiler starts again, the sessionSampleRate is reevaluated - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile() } @@ -201,7 +228,7 @@ class AndroidContinuousProfilerTest { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) } val profiler = fixture.getSut(buildInfo) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -210,7 +237,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.profilesSampleRate = 0.0 } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) } @@ -226,8 +253,8 @@ class AndroidContinuousProfilerTest { ) // Regardless of how many times the profiler is started, the option is evaluated and logged only once - profiler.start(fixture.mockTracesSampler) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockLogger, times(1)).log( SentryLevel.WARNING, "Disabling profiling because no profiling traces dir path is defined in options." @@ -247,8 +274,8 @@ class AndroidContinuousProfilerTest { ) // Regardless of how many times the profiler is started, the option is evaluated and logged only once - profiler.start(fixture.mockTracesSampler) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.mockLogger, times(1)).log( SentryLevel.WARNING, "Disabling profiling because trace rate is set to %d", @@ -261,7 +288,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.cacheDirPath = null } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -270,7 +297,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.cacheDirPath = "" } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -279,7 +306,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.profilingTracesHz = 0 } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) } @@ -290,9 +317,9 @@ class AndroidContinuousProfilerTest { it.executorService = mockExecutorService } whenever(mockExecutorService.submit(any>())).thenReturn(mock()) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(mockExecutorService, never()).submit(any()) - profiler.stop() + profiler.stopProfileSession(ProfileLifecycle.MANUAL) verify(mockExecutorService, never()).submit(any>()) } @@ -301,8 +328,8 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { File(it.profilingTracesDirPath!!).setWritable(false) } - profiler.start(fixture.mockTracesSampler) - profiler.stop() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) // We assert that no trace files are written assertTrue( File(fixture.options.profilingTracesDirPath!!) @@ -318,7 +345,7 @@ class AndroidContinuousProfilerTest { fixture.options.compositePerformanceCollector = performanceCollector val profiler = fixture.getSut() verify(performanceCollector, never()).start(any()) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(performanceCollector).start(any()) } @@ -327,9 +354,9 @@ class AndroidContinuousProfilerTest { val performanceCollector = mock() fixture.options.compositePerformanceCollector = performanceCollector val profiler = fixture.getSut() - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(performanceCollector, never()).stop(any()) - profiler.stop() + profiler.stopProfileSession(ProfileLifecycle.MANUAL) verify(performanceCollector).stop(any()) } @@ -338,16 +365,16 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut() val frameMetricsCollectorId = "id" whenever(fixture.frameMetricsCollector.startCollection(any())).thenReturn(frameMetricsCollectorId) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.frameMetricsCollector, never()).stopCollection(frameMetricsCollectorId) - profiler.stop() + profiler.stopProfileSession(ProfileLifecycle.MANUAL) verify(fixture.frameMetricsCollector).stopCollection(frameMetricsCollectorId) } @Test fun `profiler stops profiling and clear scheduled job on close`() { val profiler = fixture.getSut() - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) profiler.close() @@ -369,7 +396,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) executorService.runAll() @@ -387,7 +414,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) executorService.runAll() @@ -411,8 +438,8 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start(fixture.mockTracesSampler) - profiler.stop() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) // We run the executor service to send the profile chunk executorService.runAll() verify(fixture.scopes).captureProfileChunk( @@ -430,13 +457,13 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) 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() + profiler.stopProfileSession(ProfileLifecycle.MANUAL) // Now the executor is used to send the chunk executorService.runAll() verify(fixture.scopes, times(2)).captureProfileChunk(any()) @@ -448,7 +475,7 @@ class AndroidContinuousProfilerTest { val profiler = fixture.getSut { it.executorService = executorService } - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We close the profiler, which should prevent sending additional chunks @@ -468,7 +495,7 @@ class AndroidContinuousProfilerTest { val rateLimiter = mock() whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) // If the SDK is rate limited, the profiler should stop @@ -489,7 +516,7 @@ class AndroidContinuousProfilerTest { whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) // If the SDK is rate limited, the profiler should never start - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) assertEquals(SentryId.EMPTY_ID, profiler.profilerId) verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) @@ -506,7 +533,7 @@ class AndroidContinuousProfilerTest { } // If the device is offline, the profiler should never start - profiler.start(fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) assertEquals(SentryId.EMPTY_ID, profiler.profilerId) verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index fd736e5910..9190beb916 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.core.os.bundleOf import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ILogger +import io.sentry.ProfileLifecycle import io.sentry.SentryLevel import io.sentry.SentryReplayOptions import org.junit.runner.RunWith @@ -841,6 +842,32 @@ class ManifestMetadataReaderTest { assertNull(fixture.options.profileSessionSampleRate) } + @Test + fun `applyMetadata without specifying profileLifecycle, stays MANUAL`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(ProfileLifecycle.MANUAL, fixture.options.profileLifecycle) + } + + @Test + fun `applyMetadata reads profileLifecycle from metadata`() { + // Arrange + val expectedLifecycle = "trace" + val bundle = bundleOf(ManifestMetadataReader.PROFILE_LIFECYCLE to expectedLifecycle) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(ProfileLifecycle.TRACE, fixture.options.profileLifecycle) + } + @Test fun `applyMetadata reads tracePropagationTargets to options`() { // Arrange diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 598631725d..70f60b8350 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -443,8 +443,10 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (Z)V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V } @@ -725,8 +727,8 @@ public abstract interface class io/sentry/IContinuousProfiler { public abstract fun getProfilerId ()Lio/sentry/protocol/SentryId; public abstract fun isRunning ()Z public abstract fun reevaluateSampling ()V - public abstract fun start (Lio/sentry/TracesSampler;)V - public abstract fun stop ()V + public abstract fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public abstract fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V } public abstract interface class io/sentry/IEnvelopeReader { @@ -1409,8 +1411,8 @@ public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfi public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z public fun reevaluateSampling ()V - public fun start (Lio/sentry/TracesSampler;)V - public fun stop ()V + public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V } public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { @@ -1920,6 +1922,13 @@ public final class io/sentry/ProfileContext$JsonKeys { public fun ()V } +public final class io/sentry/ProfileLifecycle : java/lang/Enum { + public static final field MANUAL Lio/sentry/ProfileLifecycle; + public static final field TRACE Lio/sentry/ProfileLifecycle; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/ProfileLifecycle; + public static fun values ()[Lio/sentry/ProfileLifecycle; +} + 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; @@ -2489,6 +2498,7 @@ public abstract interface class io/sentry/Sentry$OptionsConfiguration { public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSampleRate ()Ljava/lang/Double; public fun getProfilingTracesDirPath ()Ljava/lang/String; public fun getProfilingTracesHz ()I @@ -2502,6 +2512,7 @@ public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSeri public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setContinuousProfileSampled (Z)V public fun setContinuousProfilingEnabled (Z)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V @@ -2522,6 +2533,7 @@ public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; 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_LIFECYCLE Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; public static final field PROFILING_TRACES_DIR_PATH Ljava/lang/String; @@ -2994,6 +3006,7 @@ public class io/sentry/SentryOptions { public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 7c8dd32853..4a99c00377 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -21,6 +21,12 @@ public final class ExperimentalOptions { */ private @Nullable Double profileSessionSampleRate; + /** + * Whether the profiling lifecycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + */ + private @NotNull ProfileLifecycle profileLifecycle = ProfileLifecycle.MANUAL; + public ExperimentalOptions(final boolean empty) { this.sessionReplay = new SentryReplayOptions(empty); } @@ -34,6 +40,26 @@ public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOpt this.sessionReplay = sessionReplayOptions; } + /** + * Returns whether the profiling cycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + * + * @return the profile lifecycle + */ + @ApiStatus.Experimental + public @NotNull ProfileLifecycle getProfileLifecycle() { + return profileLifecycle; + } + + /** Sets the profiling lifecycle. */ + @ApiStatus.Experimental + public void setProfileLifecycle(final @NotNull ProfileLifecycle profileLifecycle) { + // TODO (when moved to SentryOptions): we should log a message if the user sets this to TRACE + // and tracing is disabled + this.profileLifecycle = profileLifecycle; + } + + @ApiStatus.Experimental public @Nullable Double getProfileSessionSampleRate() { return profileSessionSampleRate; } diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java index bd37f6e14f..68cd32813c 100644 --- a/sentry/src/main/java/io/sentry/IContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -9,9 +9,10 @@ public interface IContinuousProfiler { boolean isRunning(); - void start(final @NotNull TracesSampler tracesSampler); + void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, final @NotNull TracesSampler tracesSampler); - void stop(); + void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle); /** Cancel the profiler and stops it. Used on SDK close. */ void close(); diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java index 597c3c5cfb..2e788ad6d0 100644 --- a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -14,7 +14,7 @@ public static NoOpContinuousProfiler getInstance() { } @Override - public void stop() {} + public void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) {} @Override public boolean isRunning() { @@ -22,7 +22,9 @@ public boolean isRunning() { } @Override - public void start(final @NotNull TracesSampler tracesSampler) {} + public void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) {} @Override public void close() {} diff --git a/sentry/src/main/java/io/sentry/ProfileLifecycle.java b/sentry/src/main/java/io/sentry/ProfileLifecycle.java new file mode 100644 index 0000000000..42c26e5344 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileLifecycle.java @@ -0,0 +1,18 @@ +package io.sentry; + +/** + * Determines whether the profiling lifecycle is controlled manually or based on the trace + * lifecycle. + */ +public enum ProfileLifecycle { + /** + * Profiling is controlled manually. You must use the {@link Sentry#startProfileSession()} and + * {@link Sentry#stopProfileSession()} APIs to control the lifecycle of the profiler. + */ + MANUAL, + /** + * Profiling is automatically started when there is at least 1 sampled root span, and it's + * automatically stopped when there are none. + */ + TRACE +} diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index c57a86b264..be1b63676e 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -905,15 +905,27 @@ public void flush(long timeoutMillis) { // The listener is called only if the transaction exists, as the transaction is needed to // stop it - if (samplingDecision.getSampled() && samplingDecision.getProfileSampled()) { - final ITransactionProfiler transactionProfiler = getOptions().getTransactionProfiler(); - // If the profiler is not running, we start and bind it here. - if (!transactionProfiler.isRunning()) { - transactionProfiler.start(); - transactionProfiler.bindTransaction(transaction); - } else if (transactionOptions.isAppStartTransaction()) { - // If the profiler is running and the current transaction is the app start, we bind it. - transactionProfiler.bindTransaction(transaction); + if (samplingDecision.getSampled()) { + // If transaction profiler is sampled, let's start it + if (samplingDecision.getProfileSampled()) { + final ITransactionProfiler transactionProfiler = getOptions().getTransactionProfiler(); + // If the profiler is not running, we start and bind it here. + if (!transactionProfiler.isRunning()) { + transactionProfiler.start(); + transactionProfiler.bindTransaction(transaction); + } else if (transactionOptions.isAppStartTransaction()) { + // If the profiler is running and the current transaction is the app start, we bind it. + transactionProfiler.bindTransaction(transaction); + } + } + + // If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on + // its own. + if (getOptions().isContinuousProfilingEnabled() + && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + getOptions() + .getContinuousProfiler() + .startProfileSession(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); } } } @@ -926,7 +938,18 @@ public void flush(long timeoutMillis) { @Override public void startProfileSession() { if (getOptions().isContinuousProfilingEnabled()) { - getOptions().getContinuousProfiler().start(getOptions().getInternalTracesSampler()); + if (getOptions().getProfileLifecycle() != ProfileLifecycle.MANUAL) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling lifecycle is %s. Profiling cannot be started manually.", + getOptions().getProfileLifecycle().name()); + return; + } + getOptions() + .getContinuousProfiler() + .startProfileSession(ProfileLifecycle.MANUAL, getOptions().getInternalTracesSampler()); } else if (getOptions().isProfilingEnabled()) { getOptions() .getLogger() @@ -939,8 +962,17 @@ public void startProfileSession() { @Override public void stopProfileSession() { if (getOptions().isContinuousProfilingEnabled()) { + if (getOptions().getProfileLifecycle() != ProfileLifecycle.MANUAL) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling lifecycle is %s. Profiling cannot be stopped manually.", + getOptions().getProfileLifecycle().name()); + return; + } getOptions().getLogger().log(SentryLevel.DEBUG, "Stopped continuous Profiling."); - getOptions().getContinuousProfiler().stop(); + getOptions().getContinuousProfiler().stopProfileSession(ProfileLifecycle.MANUAL); } else { getOptions() .getLogger() diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index 364a71e5ca..e764a42218 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -21,6 +21,7 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe boolean isContinuousProfilingEnabled; int profilingTracesHz; boolean continuousProfileSampled; + @NotNull ProfileLifecycle profileLifecycle; private @Nullable Map unknown; @@ -34,6 +35,7 @@ public SentryAppStartProfilingOptions() { profilingTracesDirPath = null; isProfilingEnabled = false; isContinuousProfilingEnabled = false; + profileLifecycle = ProfileLifecycle.MANUAL; profilingTracesHz = 0; } @@ -48,6 +50,7 @@ public SentryAppStartProfilingOptions() { profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); + profileLifecycle = options.getProfileLifecycle(); profilingTracesHz = options.getProfilingTracesHz(); } @@ -67,6 +70,14 @@ public boolean isContinuousProfileSampled() { return continuousProfileSampled; } + public void setProfileLifecycle(final @NotNull ProfileLifecycle profileLifecycle) { + this.profileLifecycle = profileLifecycle; + } + + public @NotNull ProfileLifecycle getProfileLifecycle() { + return profileLifecycle; + } + public void setProfileSampleRate(final @Nullable Double profileSampleRate) { this.profileSampleRate = profileSampleRate; } @@ -134,6 +145,7 @@ public static final class JsonKeys { 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 PROFILE_LIFECYCLE = "profile_lifecycle"; public static final String PROFILING_TRACES_HZ = "profiling_traces_hz"; } @@ -151,6 +163,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer .name(JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED) .value(logger, isContinuousProfilingEnabled); + writer.name(JsonKeys.PROFILE_LIFECYCLE).value(logger, profileLifecycle.name()); writer.name(JsonKeys.PROFILING_TRACES_HZ).value(logger, profilingTracesHz); if (unknown != null) { @@ -235,6 +248,18 @@ public static final class Deserializer options.isContinuousProfilingEnabled = isContinuousProfilingEnabled; } break; + case JsonKeys.PROFILE_LIFECYCLE: + String profileLifecycle = reader.nextStringOrNull(); + if (profileLifecycle != null) { + try { + options.profileLifecycle = ProfileLifecycle.valueOf(profileLifecycle); + } catch (IllegalArgumentException e) { + logger.log( + SentryLevel.ERROR, + "Error when deserializing ProfileLifecycle: " + profileLifecycle); + } + } + 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 02e34c68d4..b7b57f4beb 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1802,6 +1802,17 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { return experimental.getProfileSessionSampleRate(); } + /** + * Returns whether the profiling lifecycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + * + * @return the profile lifecycle + */ + @ApiStatus.Experimental + public @NotNull ProfileLifecycle getProfileLifecycle() { + return experimental.getProfileLifecycle(); + } + /** * Returns the profiling traces dir. path if set * diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 943dc349f2..13d59c5881 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -90,7 +90,7 @@ public SentryTracer( final @NotNull SentryId continuousProfilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); - if (!continuousProfilerId.equals(SentryId.EMPTY_ID)) { + if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) { this.contexts.setProfile(new ProfileContext(continuousProfilerId)); } @@ -246,6 +246,10 @@ public void finish( .getTransactionProfiler() .onTransactionFinish(this, performanceCollectionData.get(), scopes.getOptions()); } + if (scopes.getOptions().isContinuousProfilingEnabled() + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + scopes.getOptions().getContinuousProfiler().stopProfileSession(ProfileLifecycle.TRACE); + } if (performanceCollectionData.get() != null) { performanceCollectionData.get().clear(); } @@ -522,7 +526,7 @@ private ISpan createChild( // span.setDescription(description); final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); final SentryId profilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); - if (!profilerId.equals(SentryId.EMPTY_ID)) { + if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) { span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); } span.setData( diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 0b63116ee6..a8f1a1e80c 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1233,8 +1233,8 @@ class JsonSerializerTest { val actual = serializeToString(appStartProfilingOptions) val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"continuous_profile_sampled\":true," + - "\"trace_sampled\":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null," + - "\"is_profiling_enabled\":false,\"is_continuous_profiling_enabled\":false,\"profiling_traces_hz\":65}" + "\"trace_sampled\":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"is_continuous_profiling_enabled\":false,\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65}" assertEquals(expected, actual) } @@ -1243,7 +1243,7 @@ class JsonSerializerTest { fun `deserializing SentryAppStartProfilingOptions`() { val jsonAppStartProfilingOptions = "{\"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,\"continuous_profile_sampled\":true}" + "\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65,\"continuous_profile_sampled\":true}" val actual = fixture.serializer.deserialize(StringReader(jsonAppStartProfilingOptions), SentryAppStartProfilingOptions::class.java) assertNotNull(actual) @@ -1256,6 +1256,7 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.isContinuousProfilingEnabled, actual.isContinuousProfilingEnabled) assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) assertEquals(appStartProfilingOptions.profilingTracesDirPath, actual.profilingTracesDirPath) + assertEquals(appStartProfilingOptions.profileLifecycle, actual.profileLifecycle) assertNull(actual.unknown) } @@ -1560,6 +1561,7 @@ class JsonSerializerTest { isProfilingEnabled = false isContinuousProfilingEnabled = false profilingTracesHz = 65 + profileLifecycle = ProfileLifecycle.TRACE } private fun createSpan(): ISpan { diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt index 30ab1d090a..081c72169d 100644 --- a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -11,11 +11,11 @@ class NoOpContinuousProfilerTest { @Test fun `start does not throw`() = - profiler.start(mock()) + profiler.startProfileSession(mock(), mock()) @Test fun `stop does not throw`() = - profiler.stop() + profiler.stopProfileSession(mock()) @Test fun `isRunning returns false`() { diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 9cb8ca177a..e1e61279c2 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -53,6 +53,7 @@ class ScopesTest { private lateinit var file: File private lateinit var profilingTraceFile: File + private val mockProfiler = spy(NoOpContinuousProfiler.getInstance()) @BeforeTest fun `set up`() { @@ -772,6 +773,8 @@ class ScopesTest { } } + //endregion + //region captureCheckIn tests @Test @@ -1870,6 +1873,49 @@ class ScopesTest { val transaction = scopes.startTransaction(TransactionContext("name", "op", TracesSamplingDecision(true))) assertTrue(transaction is NoOpTransaction) } + + @Test + fun `when startTransaction, trace profile session is started`() { + val scopes = generateScopes { + it.tracesSampleRate = 1.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + + val transaction = scopes.startTransaction("name", "op") + assertTrue(transaction.isSampled!!) + verify(mockProfiler).startProfileSession(eq(ProfileLifecycle.TRACE), any()) + } + + @Test + fun `when startTransaction, manual profile session is not started`() { + val scopes = generateScopes { + it.tracesSampleRate = 1.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.MANUAL + } + + val transaction = scopes.startTransaction("name", "op") + assertTrue(transaction.isSampled!!) + verify(mockProfiler, never()).startProfileSession(any(), any()) + } + + @Test + fun `when startTransaction not sampled, trace profile session is not started`() { + val scopes = generateScopes { + // If transaction is not sampled, profiler should not start + it.tracesSampleRate = 0.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + val transaction = scopes.startTransaction("name", "op") + transaction.spanContext.setSampled(false, false) + assertFalse(transaction.isSampled!!) + verify(mockProfiler, never()).startProfileSession(any(), any()) + } //endregion //region getSpan tests @@ -2161,6 +2207,8 @@ class ScopesTest { assertEquals("other.span.origin", transaction.spanContext.origin) } + //region profileSession + @Test fun `startProfileSession starts the continuous profiler`() { val profiler = mock() @@ -2169,7 +2217,7 @@ class ScopesTest { it.experimental.profileSessionSampleRate = 1.0 } scopes.startProfileSession() - verify(profiler).start(any()) + verify(profiler).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) } @Test @@ -2184,10 +2232,26 @@ class ScopesTest { it.isDebug = true } scopes.startProfileSession() - verify(profiler, never()).start(any()) + verify(profiler, never()).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) } + @Test + fun `startProfileSession is ignored on trace lifecycle`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.setLogger(logger) + it.isDebug = true + } + scopes.startProfileSession() + verify(logger).log(eq(SentryLevel.WARNING), eq("Profiling lifecycle is %s. Profiling cannot be started manually."), eq(ProfileLifecycle.TRACE.name)) + verify(profiler, never()).startProfileSession(any(), any()) + } + @Test fun `stopProfileSession stops the continuous profiler`() { val profiler = mock() @@ -2196,7 +2260,7 @@ class ScopesTest { it.experimental.profileSessionSampleRate = 1.0 } scopes.stopProfileSession() - verify(profiler).stop() + verify(profiler).stopProfileSession(eq(ProfileLifecycle.MANUAL)) } @Test @@ -2211,10 +2275,28 @@ class ScopesTest { it.isDebug = true } scopes.stopProfileSession() - verify(profiler, never()).stop() + verify(profiler, never()).stopProfileSession(eq(ProfileLifecycle.MANUAL)) verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) } + @Test + fun `stopProfileSession is ignored on trace lifecycle`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.setLogger(logger) + it.isDebug = true + } + scopes.stopProfileSession() + verify(logger).log(eq(SentryLevel.WARNING), eq("Profiling lifecycle is %s. Profiling cannot be stopped manually."), eq(ProfileLifecycle.TRACE.name)) + verify(profiler, never()).stopProfileSession(any()) + } + + //endregion + 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 721d4c3fa2..22aec0c9a0 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -289,6 +289,20 @@ class SentryOptionsTest { assertFailsWith { SentryOptions().experimental.profileSessionSampleRate = -0.0000000000001 } } + @Test + fun `when profileLifecycleSessionSampleRate is set to a value, value is set`() { + val options = SentryOptions().apply { + this.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle) + } + + @Test + fun `profileLifecycleSessionSampleRate defaults to MANUAL`() { + val options = SentryOptions() + assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle) + } + @Test fun `when options is initialized, compositePerformanceCollector is set`() { assertIs(SentryOptions().compositePerformanceCollector) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index ef337dff90..cf7f7384f8 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1315,7 +1315,7 @@ class SentryTest { it.experimental.profileSessionSampleRate = 1.0 } Sentry.startProfileSession() - verify(profiler).start(any()) + verify(profiler).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) } @Test @@ -1327,7 +1327,28 @@ class SentryTest { it.profilesSampleRate = 1.0 } Sentry.startProfileSession() - verify(profiler, never()).start(any()) + verify(profiler, never()).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) + } + + @Test + fun `startProfileSession is ignored when profile lifecycle is TRACE`() { + val profiler = mock() + val logger = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.isDebug = true + it.setLogger(logger) + } + Sentry.startProfileSession() + verify(profiler, never()).startProfileSession(any(), any()) + verify(logger).log( + eq(SentryLevel.WARNING), + eq("Profiling lifecycle is %s. Profiling cannot be started manually."), + eq(ProfileLifecycle.TRACE.name) + ) } @Test @@ -1339,7 +1360,7 @@ class SentryTest { it.experimental.profileSessionSampleRate = 1.0 } Sentry.stopProfileSession() - verify(profiler).stop() + verify(profiler).stopProfileSession(eq(ProfileLifecycle.MANUAL)) } @Test @@ -1351,7 +1372,7 @@ class SentryTest { it.profilesSampleRate = 1.0 } Sentry.stopProfileSession() - verify(profiler, never()).stop() + verify(profiler, never()).stopProfileSession(eq(ProfileLifecycle.MANUAL)) } private class InMemoryOptionsObserver : IOptionsObserver { diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 2be549cba6..ed0c3bdc90 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -220,7 +220,7 @@ class SentryTracerTest { whenever(continuousProfiler.profilerId).thenReturn(profilerId) val tracer = fixture.getSut(optionsConfiguration = { it.setContinuousProfiler(continuousProfiler) - }) + }, samplingDecision = TracesSamplingDecision(true)) tracer.finish() verify(fixture.scopes).captureTransaction( check { @@ -234,6 +234,42 @@ class SentryTracerTest { ) } + @Test + fun `when transaction is not sampled, profile context is not set`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + }, samplingDecision = TracesSamplingDecision(false)) + tracer.finish() + // profiler is never stopped, as it was never started + verify(continuousProfiler, never()).stopProfileSession(any()) + // profile context is not set + verify(fixture.scopes).captureTransaction( + check { + assertNull(it.contexts.profile) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is running in MANUAL mode, profiler is not stopped on transaction finish`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + it.experimental.profileLifecycle = ProfileLifecycle.MANUAL + }, samplingDecision = TracesSamplingDecision(true)) + tracer.finish() + // profiler is never stopped, as it should be stopped manually + verify(continuousProfiler, never()).stopProfileSession(any()) + } + @Test fun `when continuous profiler is not running, profile context is not set`() { val tracer = fixture.getSut(optionsConfiguration = { @@ -258,11 +294,24 @@ class SentryTracerTest { val tracer = fixture.getSut(optionsConfiguration = { options -> options.setContinuousProfiler(profiler) - }) + }, samplingDecision = TracesSamplingDecision(true)) val span = tracer.startChild("span.op") assertEquals(profilerId.toString(), span.getData(SpanDataConvention.PROFILER_ID)) } + @Test + fun `when transaction is not sampled, profiler id is NOT set in span data`() { + val profilerId = SentryId() + val profiler = mock() + whenever(profiler.profilerId).thenReturn(profilerId) + + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(profiler) + }, samplingDecision = TracesSamplingDecision(false)) + val span = tracer.startChild("span.op") + assertNull(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 -> From 3d0fb3c166d070c06989a87602baa0d32a1510ce Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 14 Mar 2025 18:08:27 +0100 Subject: [PATCH 11/14] Add Continuous Profiling isStartProfilerOnAppStart option (#4226) * added isStartProfilerOnAppStart experimental option --- .../android/core/ManifestMetadataReader.java | 32 ++++++---- .../core/SentryPerformanceProvider.java | 8 ++- .../core/ManifestMetadataReaderTest.kt | 25 ++++++++ .../core/SentryPerformanceProviderTest.kt | 20 +++++++ .../src/main/AndroidManifest.xml | 8 ++- sentry/api/sentry.api | 9 +++ .../java/io/sentry/ExperimentalOptions.java | 21 +++++++ sentry/src/main/java/io/sentry/Sentry.java | 13 ++++- .../SentryAppStartProfilingOptions.java | 58 +++++++++++++++---- .../main/java/io/sentry/SentryOptions.java | 8 +++ .../test/java/io/sentry/JsonSerializerTest.kt | 11 +++- .../test/java/io/sentry/SentryOptionsTest.kt | 14 +++++ sentry/src/test/java/io/sentry/SentryTest.kt | 41 ++++++++++++- 13 files changed, 236 insertions(+), 32 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index e85e5de923..bf1eb79be8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -70,6 +70,8 @@ final class ManifestMetadataReader { static final String PROFILE_LIFECYCLE = "io.sentry.traces.profiling.lifecycle"; + static final String PROFILER_START_ON_APP_START = "io.sentry.traces.profiling.start-on-app-start"; + @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; @@ -137,7 +139,7 @@ static void applyMetadata( options.setDebug(readBool(metadata, logger, DEBUG, options.isDebug())); if (options.isDebug()) { - final String level = + final @Nullable String level = readString( metadata, logger, @@ -159,7 +161,7 @@ static void applyMetadata( options.isEnableAutoSessionTracking())); if (options.getSampleRate() == null) { - final Double sampleRate = readDouble(metadata, logger, SAMPLE_RATE); + final double sampleRate = readDouble(metadata, logger, SAMPLE_RATE); if (sampleRate != -1) { options.setSampleRate(sampleRate); } @@ -178,7 +180,7 @@ static void applyMetadata( options.setAttachAnrThreadDump( readBool(metadata, logger, ANR_ATTACH_THREAD_DUMPS, options.isAttachAnrThreadDump())); - final String dsn = readString(metadata, logger, DSN, options.getDsn()); + final @Nullable String dsn = readString(metadata, logger, DSN, options.getDsn()); final boolean enabled = readBool(metadata, logger, ENABLE_SENTRY, options.isEnabled()); if (!enabled || (dsn != null && dsn.isEmpty())) { @@ -291,7 +293,7 @@ static void applyMetadata( options.isCollectAdditionalContext())); if (options.getTracesSampleRate() == null) { - final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); + final double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); if (tracesSampleRate != -1) { options.setTracesSampleRate(tracesSampleRate); } @@ -315,7 +317,7 @@ static void applyMetadata( options.isEnableActivityLifecycleTracingAutoFinish())); if (options.getProfilesSampleRate() == null) { - final Double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE); + final double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE); if (profilesSampleRate != -1) { options.setProfilesSampleRate(profilesSampleRate); } @@ -329,7 +331,7 @@ static void applyMetadata( } } - final String profileLifecycle = + final @Nullable String profileLifecycle = readString( metadata, logger, @@ -342,6 +344,15 @@ static void applyMetadata( ProfileLifecycle.valueOf(profileLifecycle.toUpperCase(Locale.ROOT))); } + options + .getExperimental() + .setStartProfilerOnAppStart( + readBool( + metadata, + logger, + PROFILER_START_ON_APP_START, + options.isStartProfilerOnAppStart())); + options.setEnableUserInteractionTracing( readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); @@ -382,6 +393,7 @@ static void applyMetadata( // sdkInfo.addIntegration(); + @Nullable List integrationsFromGradlePlugin = readList(metadata, logger, SENTRY_GRADLE_PLUGIN_INTEGRATIONS); if (integrationsFromGradlePlugin != null) { @@ -407,7 +419,7 @@ static void applyMetadata( metadata, logger, ENABLE_SCOPE_PERSISTENCE, options.isEnableScopePersistence())); if (options.getExperimental().getSessionReplay().getSessionSampleRate() == null) { - final Double sessionSampleRate = + final double sessionSampleRate = readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE); if (sessionSampleRate != -1) { options.getExperimental().getSessionReplay().setSessionSampleRate(sessionSampleRate); @@ -415,7 +427,7 @@ static void applyMetadata( } if (options.getExperimental().getSessionReplay().getOnErrorSampleRate() == null) { - final Double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE); + final double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE); if (onErrorSampleRate != -1) { options.getExperimental().getSessionReplay().setOnErrorSampleRate(onErrorSampleRate); } @@ -501,10 +513,10 @@ private static boolean readBool( } } - private static @NotNull Double readDouble( + private static double readDouble( final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { // manifest meta-data only reads float - final Double value = ((Number) metadata.getFloat(key, metadata.getInt(key, -1))).doubleValue(); + final double value = ((Number) metadata.getFloat(key, metadata.getInt(key, -1))).doubleValue(); logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } 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 df05f880d8..93801b0cf5 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 @@ -139,7 +139,8 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } - if (profilingOptions.isContinuousProfilingEnabled()) { + if (profilingOptions.isContinuousProfilingEnabled() + && profilingOptions.isStartProfilerOnAppStart()) { createAndStartContinuousProfiler(context, profilingOptions, appStartMetrics); return; } @@ -150,8 +151,9 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } - createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics); - + if (profilingOptions.isEnableAppStartProfiling()) { + createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics); + } } catch (FileNotFoundException e) { logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e); } catch (Throwable e) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 9190beb916..40143a74c3 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -868,6 +868,31 @@ class ManifestMetadataReaderTest { assertEquals(ProfileLifecycle.TRACE, fixture.options.profileLifecycle) } + @Test + fun `applyMetadata without specifying isStartProfilerOnAppStart, stays false`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.isStartProfilerOnAppStart) + } + + @Test + fun `applyMetadata reads isStartProfilerOnAppStart from metadata`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.PROFILER_START_ON_APP_START to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.isStartProfilerOnAppStart) + } + @Test fun `applyMetadata reads tracePropagationTargets to options`() { // Arrange 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 16cce1a209..83f5795f13 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 @@ -326,6 +326,22 @@ class SentryPerformanceProviderTest { assertFalse(AppStartMetrics.getInstance().appStartProfiler!!.isRunning) } + @Test + fun `when isEnableAppStartProfiling is false, transaction profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = true, continuousProfilingEnabled = false, isEnableAppStartProfiling = false) + } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + } + + @Test + fun `when isStartProfilerOnAppStart is false, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = false, continuousProfilingEnabled = true, isStartProfilerOnAppStart = false) + } + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + @Test fun `when provider is closed, continuous profiler is stopped`() { val provider = fixture.getSut { config -> @@ -345,6 +361,8 @@ class SentryPerformanceProviderTest { profileSampled: Boolean = true, profileSampleRate: Double = 1.0, continuousProfileSampled: Boolean = true, + isEnableAppStartProfiling: Boolean = true, + isStartProfilerOnAppStart: Boolean = true, profilingTracesDirPath: String = traceDir.absolutePath ) { val appStartProfilingOptions = SentryAppStartProfilingOptions() @@ -357,6 +375,8 @@ class SentryPerformanceProviderTest { appStartProfilingOptions.isContinuousProfileSampled = continuousProfileSampled appStartProfilingOptions.profilingTracesDirPath = profilingTracesDirPath appStartProfilingOptions.profilingTracesHz = 101 + appStartProfilingOptions.isEnableAppStartProfiling = isEnableAppStartProfiling + appStartProfilingOptions.isStartProfilerOnAppStart = isStartProfilerOnAppStart JsonSerializer(SentryOptions.empty()).serialize(appStartProfilingOptions, FileWriter(configFile)) } //endregion diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 5afe6ac180..03fb4e5f20 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -112,8 +112,12 @@ - - + + + + + + diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 70f60b8350..31a4d7d6da 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -446,9 +446,11 @@ public final class io/sentry/ExperimentalOptions { public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; + public fun isStartProfilerOnAppStart ()Z public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V + public fun setStartProfilerOnAppStart (Z)V } public final class io/sentry/ExternalOptions { @@ -2506,18 +2508,22 @@ public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSeri public fun getUnknown ()Ljava/util/Map; public fun isContinuousProfileSampled ()Z public fun isContinuousProfilingEnabled ()Z + public fun isEnableAppStartProfiling ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setContinuousProfileSampled (Z)V public fun setContinuousProfilingEnabled (Z)V + public fun setEnableAppStartProfiling (Z)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V + public fun setStartProfilerOnAppStart (Z)V public fun setTraceSampleRate (Ljava/lang/Double;)V public fun setTraceSampled (Z)V public fun setUnknown (Ljava/util/Map;)V @@ -2532,7 +2538,9 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_ENABLE_APP_START_PROFILING Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_START_PROFILER_ON_APP_START Ljava/lang/String; public static final field PROFILE_LIFECYCLE Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; @@ -3065,6 +3073,7 @@ public class io/sentry/SentryOptions { public fun isSendClientReports ()Z public fun isSendDefaultPii ()Z public fun isSendModules ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 4a99c00377..fc93416d02 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -27,6 +27,17 @@ public final class ExperimentalOptions { */ private @NotNull ProfileLifecycle profileLifecycle = ProfileLifecycle.MANUAL; + /** + * Whether profiling can automatically be started as early as possible during the app lifecycle, + * to capture more of app startup. If {@link ExperimentalOptions#profileLifecycle} is {@link + * ProfileLifecycle#MANUAL} Profiling is started automatically on startup and stopProfileSession + * must be called manually whenever the app startup is completed If {@link + * ExperimentalOptions#profileLifecycle} is {@link ProfileLifecycle#TRACE} Profiling is started + * automatically on startup, and will automatically be stopped when the root span that is + * associated with app startup ends + */ + private boolean startProfilerOnAppStart = false; + public ExperimentalOptions(final boolean empty) { this.sessionReplay = new SentryReplayOptions(empty); } @@ -74,4 +85,14 @@ public void setProfileSessionSampleRate(final @Nullable Double profileSessionSam } this.profileSessionSampleRate = profileSessionSampleRate; } + + @ApiStatus.Experimental + public boolean isStartProfilerOnAppStart() { + return startProfilerOnAppStart; + } + + @ApiStatus.Experimental + public void setStartProfilerOnAppStart(boolean startProfilerOnAppStart) { + this.startProfilerOnAppStart = startProfilerOnAppStart; + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 02e7d168c7..a3ecbacf8d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -370,10 +370,12 @@ private static void handleAppStartProfilingConfig( try { // We always delete the config file for app start profiling FileUtils.deleteRecursively(appStartProfilingConfigFile); - if (!options.isEnableAppStartProfiling()) { + if (!options.isEnableAppStartProfiling() && !options.isStartProfilerOnAppStart()) { return; } - if (!options.isTracingEnabled()) { + // isStartProfilerOnAppStart doesn't need tracing, as it can be started/stopped + // manually + if (!options.isStartProfilerOnAppStart() && !options.isTracingEnabled()) { options .getLogger() .log( @@ -382,8 +384,13 @@ private static void handleAppStartProfilingConfig( return; } if (appStartProfilingConfigFile.createNewFile()) { + // If old app start profiling is false, it means the transaction will not be + // sampled, but we create the file anyway to allow continuous profiling on app + // start final @NotNull TracesSamplingDecision appStartSamplingDecision = - sampleAppStartProfiling(options); + options.isEnableAppStartProfiling() + ? sampleAppStartProfiling(options) + : new TracesSamplingDecision(false); final @NotNull SentryAppStartProfilingOptions appStartProfilingOptions = new SentryAppStartProfilingOptions(options, appStartSamplingDecision); try (final OutputStream outputStream = diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index e764a42218..3c16504eb7 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -21,6 +21,8 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe boolean isContinuousProfilingEnabled; int profilingTracesHz; boolean continuousProfileSampled; + boolean isEnableAppStartProfiling; + boolean isStartProfilerOnAppStart; @NotNull ProfileLifecycle profileLifecycle; private @Nullable Map unknown; @@ -37,6 +39,8 @@ public SentryAppStartProfilingOptions() { isContinuousProfilingEnabled = false; profileLifecycle = ProfileLifecycle.MANUAL; profilingTracesHz = 0; + isEnableAppStartProfiling = true; + isStartProfilerOnAppStart = false; } SentryAppStartProfilingOptions( @@ -52,6 +56,8 @@ public SentryAppStartProfilingOptions() { isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); profileLifecycle = options.getProfileLifecycle(); profilingTracesHz = options.getProfilingTracesHz(); + isEnableAppStartProfiling = options.isEnableAppStartProfiling(); + isStartProfilerOnAppStart = options.isStartProfilerOnAppStart(); } public void setProfileSampled(final boolean profileSampled) { @@ -134,6 +140,22 @@ public int getProfilingTracesHz() { return profilingTracesHz; } + public void setEnableAppStartProfiling(final boolean enableAppStartProfiling) { + isEnableAppStartProfiling = enableAppStartProfiling; + } + + public boolean isEnableAppStartProfiling() { + return isEnableAppStartProfiling; + } + + public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { + isStartProfilerOnAppStart = startProfilerOnAppStart; + } + + public boolean isStartProfilerOnAppStart() { + return isStartProfilerOnAppStart; + } + // JsonSerializable public static final class JsonKeys { @@ -147,6 +169,8 @@ public static final class JsonKeys { public static final String IS_CONTINUOUS_PROFILING_ENABLED = "is_continuous_profiling_enabled"; public static final String PROFILE_LIFECYCLE = "profile_lifecycle"; public static final String PROFILING_TRACES_HZ = "profiling_traces_hz"; + public static final String IS_ENABLE_APP_START_PROFILING = "is_enable_app_start_profiling"; + public static final String IS_START_PROFILER_ON_APP_START = "is_start_profiler_on_app_start"; } @Override @@ -165,6 +189,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger .value(logger, isContinuousProfilingEnabled); writer.name(JsonKeys.PROFILE_LIFECYCLE).value(logger, profileLifecycle.name()); writer.name(JsonKeys.PROFILING_TRACES_HZ).value(logger, profilingTracesHz); + writer.name(JsonKeys.IS_ENABLE_APP_START_PROFILING).value(logger, isEnableAppStartProfiling); + writer.name(JsonKeys.IS_START_PROFILER_ON_APP_START).value(logger, isStartProfilerOnAppStart); if (unknown != null) { for (String key : unknown.keySet()) { @@ -201,55 +227,55 @@ public static final class Deserializer final String nextName = reader.nextName(); switch (nextName) { case JsonKeys.PROFILE_SAMPLED: - Boolean profileSampled = reader.nextBooleanOrNull(); + @Nullable Boolean profileSampled = reader.nextBooleanOrNull(); if (profileSampled != null) { options.profileSampled = profileSampled; } break; case JsonKeys.PROFILE_SAMPLE_RATE: - Double profileSampleRate = reader.nextDoubleOrNull(); + @Nullable Double profileSampleRate = reader.nextDoubleOrNull(); if (profileSampleRate != null) { options.profileSampleRate = profileSampleRate; } break; case JsonKeys.CONTINUOUS_PROFILE_SAMPLED: - Boolean continuousProfileSampled = reader.nextBooleanOrNull(); + @Nullable Boolean continuousProfileSampled = reader.nextBooleanOrNull(); if (continuousProfileSampled != null) { options.continuousProfileSampled = continuousProfileSampled; } break; case JsonKeys.TRACE_SAMPLED: - Boolean traceSampled = reader.nextBooleanOrNull(); + @Nullable Boolean traceSampled = reader.nextBooleanOrNull(); if (traceSampled != null) { options.traceSampled = traceSampled; } break; case JsonKeys.TRACE_SAMPLE_RATE: - Double traceSampleRate = reader.nextDoubleOrNull(); + @Nullable Double traceSampleRate = reader.nextDoubleOrNull(); if (traceSampleRate != null) { options.traceSampleRate = traceSampleRate; } break; case JsonKeys.PROFILING_TRACES_DIR_PATH: - String profilingTracesDirPath = reader.nextStringOrNull(); + @Nullable String profilingTracesDirPath = reader.nextStringOrNull(); if (profilingTracesDirPath != null) { options.profilingTracesDirPath = profilingTracesDirPath; } break; case JsonKeys.IS_PROFILING_ENABLED: - Boolean isProfilingEnabled = reader.nextBooleanOrNull(); + @Nullable Boolean isProfilingEnabled = reader.nextBooleanOrNull(); if (isProfilingEnabled != null) { options.isProfilingEnabled = isProfilingEnabled; } break; case JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED: - Boolean isContinuousProfilingEnabled = reader.nextBooleanOrNull(); + @Nullable Boolean isContinuousProfilingEnabled = reader.nextBooleanOrNull(); if (isContinuousProfilingEnabled != null) { options.isContinuousProfilingEnabled = isContinuousProfilingEnabled; } break; case JsonKeys.PROFILE_LIFECYCLE: - String profileLifecycle = reader.nextStringOrNull(); + @Nullable String profileLifecycle = reader.nextStringOrNull(); if (profileLifecycle != null) { try { options.profileLifecycle = ProfileLifecycle.valueOf(profileLifecycle); @@ -261,11 +287,23 @@ public static final class Deserializer } break; case JsonKeys.PROFILING_TRACES_HZ: - Integer profilingTracesHz = reader.nextIntegerOrNull(); + @Nullable Integer profilingTracesHz = reader.nextIntegerOrNull(); if (profilingTracesHz != null) { options.profilingTracesHz = profilingTracesHz; } break; + case JsonKeys.IS_ENABLE_APP_START_PROFILING: + @Nullable Boolean isEnableAppStartProfiling = reader.nextBooleanOrNull(); + if (isEnableAppStartProfiling != null) { + options.isEnableAppStartProfiling = isEnableAppStartProfiling; + } + break; + case JsonKeys.IS_START_PROFILER_ON_APP_START: + @Nullable Boolean isStartProfilerOnAppStart = reader.nextBooleanOrNull(); + if (isStartProfilerOnAppStart != null) { + options.isStartProfilerOnAppStart = isStartProfilerOnAppStart; + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index b7b57f4beb..c7087dbb36 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1813,6 +1813,14 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { return experimental.getProfileLifecycle(); } + /** + * Whether profiling can automatically be started as early as possible during the app lifecycle. + */ + @ApiStatus.Experimental + public boolean isStartProfilerOnAppStart() { + return experimental.isStartProfilerOnAppStart(); + } + /** * Returns the profiling traces dir. path if set * diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index a8f1a1e80c..38a3f49f24 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1234,8 +1234,8 @@ class JsonSerializerTest { val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"continuous_profile_sampled\":true," + "\"trace_sampled\":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + - "\"is_continuous_profiling_enabled\":false,\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65}" - + "\"is_continuous_profiling_enabled\":false,\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65," + + "\"is_enable_app_start_profiling\":false,\"is_start_profiler_on_app_start\":true}" assertEquals(expected, actual) } @@ -1243,7 +1243,8 @@ class JsonSerializerTest { fun `deserializing SentryAppStartProfilingOptions`() { val jsonAppStartProfilingOptions = "{\"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," + - "\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65,\"continuous_profile_sampled\":true}" + "\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65,\"continuous_profile_sampled\":true," + + "\"is_enable_app_start_profiling\":false,\"is_start_profiler_on_app_start\":true}" val actual = fixture.serializer.deserialize(StringReader(jsonAppStartProfilingOptions), SentryAppStartProfilingOptions::class.java) assertNotNull(actual) @@ -1257,6 +1258,8 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) assertEquals(appStartProfilingOptions.profilingTracesDirPath, actual.profilingTracesDirPath) assertEquals(appStartProfilingOptions.profileLifecycle, actual.profileLifecycle) + assertEquals(appStartProfilingOptions.isEnableAppStartProfiling, actual.isEnableAppStartProfiling) + assertEquals(appStartProfilingOptions.isStartProfilerOnAppStart, actual.isStartProfilerOnAppStart) assertNull(actual.unknown) } @@ -1562,6 +1565,8 @@ class JsonSerializerTest { isContinuousProfilingEnabled = false profilingTracesHz = 65 profileLifecycle = ProfileLifecycle.TRACE + isEnableAppStartProfiling = false + isStartProfilerOnAppStart = true } private fun createSpan(): ISpan { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 22aec0c9a0..fc850e4bd4 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -303,6 +303,20 @@ class SentryOptionsTest { assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle) } + @Test + fun `when isStartProfilerOnAppStart is set to a value, value is set`() { + val options = SentryOptions().apply { + this.experimental.isStartProfilerOnAppStart = true + } + assertTrue(options.isStartProfilerOnAppStart) + } + + @Test + fun `isStartProfilerOnAppStart defaults to false`() { + val options = SentryOptions() + assertFalse(options.isStartProfilerOnAppStart) + } + @Test fun `when options is initialized, compositePerformanceCollector is set`() { assertIs(SentryOptions().compositePerformanceCollector) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index cf7f7384f8..bf68c63d07 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1130,6 +1130,25 @@ class SentryTest { ) } + @Test + fun `init calls samplers if isStartProfilerOnAppStart is true`() { + val mockSampleTracer = mock() + val mockProfilesSampler = mock() + Sentry.init { + it.dsn = dsn + it.tracesSampleRate = 1.0 + it.experimental.isStartProfilerOnAppStart = true + it.profilesSampleRate = 1.0 + it.tracesSampler = mockSampleTracer + it.profilesSampler = mockProfilesSampler + it.executorService = ImmediateExecutorService() + it.cacheDirPath = getTempPath() + } + // Samplers are not called + verify(mockSampleTracer, never()).sample(any()) + verify(mockProfilesSampler, never()).sample(any()) + } + @Test fun `init calls app start profiling samplers in the background`() { val mockSampleTracer = mock() @@ -1220,6 +1239,24 @@ class SentryTest { assertTrue(appStartProfilingConfigFile.exists()) } + @Test + fun `init creates app start profiling config if isStartProfilerOnAppStart, even with performance disabled`() { + val path = getTempPath() + File(path).mkdirs() + val appStartProfilingConfigFile = File(path, "app_start_profiling_config") + appStartProfilingConfigFile.createNewFile() + assertTrue(appStartProfilingConfigFile.exists()) + Sentry.init { + it.dsn = dsn + it.cacheDirPath = path + it.isEnableAppStartProfiling = false + it.experimental.isStartProfilerOnAppStart = true + it.tracesSampleRate = 0.0 + it.executorService = ImmediateExecutorService() + } + assertTrue(appStartProfilingConfigFile.exists()) + } + @Test fun `init saves SentryAppStartProfilingOptions to disk`() { var options = SentryOptions() @@ -1227,9 +1264,9 @@ class SentryTest { Sentry.init { it.dsn = dsn it.cacheDirPath = path - it.tracesSampleRate = 1.0 it.tracesSampleRate = 0.5 it.isEnableAppStartProfiling = true + it.experimental.isStartProfilerOnAppStart = true it.profilesSampleRate = 0.2 it.executorService = ImmediateExecutorService() options = it @@ -1242,6 +1279,8 @@ class SentryTest { assertEquals(0.5, appStartOption.traceSampleRate) assertEquals(0.2, appStartOption.profileSampleRate) assertTrue(appStartOption.isProfilingEnabled) + assertTrue(appStartOption.isEnableAppStartProfiling) + assertTrue(appStartOption.isStartProfilerOnAppStart) } @Test From c7164d7949d9d384143afa0c6acd0f02cd1ef170 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 17 Mar 2025 19:00:24 +0100 Subject: [PATCH 12/14] merged main and updated tests --- CHANGELOG.md | 8 + gradle/wrapper/gradle-wrapper.properties | 3 +- .../core/AndroidContinuousProfiler.java | 3 +- .../core/SentryPerformanceProvider.java | 1 - .../core/AndroidContinuousProfilerTest.kt | 29 ++- .../core/performance/AppStartMetricsTest.kt | 2 +- .../webflux/SentryWebFluxTracingFilterTest.kt | 2 +- sentry/api/sentry.api | 246 +++++++++++++++--- .../java/io/sentry/ExperimentalOptions.java | 2 - .../SentryAppStartProfilingOptions.java | 6 +- .../src/main/java/io/sentry/SentryTracer.java | 1 - .../main/java/io/sentry/TracesSampler.java | 5 +- .../test/java/io/sentry/SpanContextTest.kt | 3 +- .../test/java/io/sentry/TracesSamplerTest.kt | 16 +- 14 files changed, 261 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52241f24e6..2b403aecfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,16 @@ Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling. ```java + import io.sentry.ProfileLifecycle; import io.sentry.android.core.SentryAndroid; SentryAndroid.init(context) { options -> // Currently under experimental options: options.getExperimental().setProfileSessionSampleRate(1.0); + // In manual mode, you need to start and stop the profiler manually using Sentry.startProfileSession and Sentry.stopProfileSession + // In trace mode, the profiler will start and stop automatically whenever a sampled trace starts and finishes + options.getExperimental().setProfileLifecycle(ProfileLifecycle.MANUAL); } // Start profiling Sentry.startProfileSession(); @@ -24,12 +28,16 @@ Sentry.stopProfileSession(); ``` ```kotlin + import io.sentry.ProfileLifecycle import io.sentry.android.core.SentryAndroid SentryAndroid.init(context) { options -> // Currently under experimental options: options.experimental.profileSessionSampleRate = 1.0 + // In manual mode, you need to start and stop the profiler manually using Sentry.startProfileSession and Sentry.stopProfileSession + // In trace mode, the profiler will start and stop automatically whenever a sampled trace starts and finishes + options.experimental.profileLifecycle = ProfileLifecycle.MANUAL } // Start profiling Sentry.startProfileSession() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c8..1e172b89f5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ +#Mon Mar 17 13:40:54 CET 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME 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 3f840f2179..632a6a4bf2 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 @@ -24,6 +24,7 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.SentryId; import io.sentry.transport.RateLimiter; +import io.sentry.util.SentryRandom; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; @@ -109,7 +110,7 @@ public synchronized void startProfileSession( final @NotNull ProfileLifecycle profileLifecycle, final @NotNull TracesSampler tracesSampler) { if (shouldSample) { - isSampled = tracesSampler.sampleSessionProfile(); + isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); shouldSample = false; } if (!isSampled) { 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 7328892ea5..a6d12f4f5b 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 @@ -9,7 +9,6 @@ import android.net.Uri; import android.os.Process; import android.os.SystemClock; -import androidx.annotation.NonNull; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; 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 e77d82fe46..542792b8a9 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 @@ -27,6 +27,8 @@ import io.sentry.test.DeferredExecutorService import io.sentry.test.getProperty import io.sentry.transport.RateLimiter import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.eq @@ -59,6 +61,7 @@ class AndroidContinuousProfilerTest { val buildInfo = mock { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) } + val mockedSentry = mockStatic(Sentry::class.java) val mockLogger = mock() val mockTracesSampler = mock() @@ -77,7 +80,7 @@ class AndroidContinuousProfilerTest { } init { - whenever(mockTracesSampler.sampleSessionProfile()).thenReturn(true) + whenever(mockTracesSampler.sampleSessionProfile(any())).thenReturn(true) } fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { @@ -133,11 +136,14 @@ class AndroidContinuousProfilerTest { File(fixture.options.profilingTracesDirPath!!).mkdirs() Sentry.setCurrentScopes(fixture.scopes) + + fixture.mockedSentry.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) } @AfterTest fun clear() { context.cacheDir.deleteRecursively() + fixture.mockedSentry.close() } @Test @@ -189,7 +195,7 @@ class AndroidContinuousProfilerTest { @Test fun `profiler logs a warning on start if not sampled`() { val profiler = fixture.getSut() - whenever(fixture.mockTracesSampler.sampleSessionProfile()).thenReturn(false) + whenever(fixture.mockTracesSampler.sampleSessionProfile(any())).thenReturn(false) profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertFalse(profiler.isRunning) verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) @@ -198,28 +204,28 @@ class AndroidContinuousProfilerTest { @Test fun `profiler evaluates sessionSampleRate only the first time`() { val profiler = fixture.getSut() - verify(fixture.mockTracesSampler, never()).sampleSessionProfile() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) // The first time the profiler is started, the sessionSampleRate is evaluated profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) // Then, the sessionSampleRate is not evaluated again profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) } @Test fun `when reevaluateSampling, profiler evaluates sessionSampleRate on next start`() { val profiler = fixture.getSut() - verify(fixture.mockTracesSampler, never()).sampleSessionProfile() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) // The first time the profiler is started, the sessionSampleRate is evaluated profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) // When reevaluateSampling is called, the sessionSampleRate is not evaluated immediately profiler.reevaluateSampling() - verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) // Then, when the profiler starts again, the sessionSampleRate is reevaluated profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile() + verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile(any()) } @Test @@ -538,4 +544,9 @@ class AndroidContinuousProfilerTest { assertEquals(SentryId.EMPTY_ID, profiler.profilerId) verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } } 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 7d9ed27d32..f8734a26f0 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 @@ -269,7 +269,7 @@ class AppStartMetricsTest { whenever(profiler.isRunning).thenReturn(true) AppStartMetrics.getInstance().appStartContinuousProfiler = profiler - AppStartMetrics.getInstance().registerApplicationForegroundCheck(mock()) + AppStartMetrics.getInstance().registerLifecycleCallbacks(mock()) // Job on main thread checks if activity was launched Shadows.shadowOf(Looper.getMainLooper()).idle() 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 6eba98d1d2..5a30bc86c9 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(3)).options + verify(fixture.scopes, times(4)).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 5115cace13..b8e7806361 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -324,10 +324,20 @@ 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 (JD)V + public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D - public fun getTimestampMillis ()J + public fun getTimestamp ()Lio/sentry/SentryDate; } public final class io/sentry/CustomSamplingContext { @@ -344,6 +354,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Error Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; + public static final field ProfileChunk Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; public static final field Session Lio/sentry/DataCategory; @@ -380,6 +391,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 @@ -391,16 +413,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 { @@ -443,6 +456,12 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (ZLio/sentry/protocol/SdkVersion;)V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; + public fun isStartProfilerOnAppStart ()Z + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V + public fun setProfileSessionSampleRate (Ljava/lang/Double;)V + public fun setStartProfilerOnAppStart (Z)V } public final class io/sentry/ExternalOptions { @@ -586,6 +605,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 @@ -632,8 +652,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -651,6 +673,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 @@ -696,8 +719,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -722,6 +747,15 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V } +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 reevaluateSampling ()V + public abstract fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public abstract fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -874,6 +908,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; @@ -923,11 +958,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 startProfileSession ()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 stopProfileSession ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -953,6 +990,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 @@ -1033,7 +1071,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 { @@ -1267,9 +1305,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 getTimestampMillis ()J + public fun (JJLio/sentry/SentryDate;)V + public fun (JLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J } @@ -1373,6 +1411,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 @@ -1381,6 +1430,16 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V } +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 reevaluateSampling ()V + public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -1398,6 +1457,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 @@ -1445,8 +1505,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1557,6 +1619,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 @@ -1604,8 +1667,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1666,7 +1731,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; } @@ -1723,15 +1788,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 @@ -1817,6 +1873,87 @@ 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;Ljava/lang/Double;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 getTimestamp ()D + 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;Lio/sentry/SentryDate;)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 TIMESTAMP Ljava/lang/String; + public static final field VERSION Ljava/lang/String; + public fun ()V +} + +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 + 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/ProfileLifecycle : java/lang/Enum { + public static final field MANUAL Lio/sentry/ProfileLifecycle; + public static final field TRACE Lio/sentry/ProfileLifecycle; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/ProfileLifecycle; + public static fun values ()[Lio/sentry/ProfileLifecycle; +} + 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; @@ -2160,6 +2297,7 @@ public final class io/sentry/Scopes : 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 @@ -2206,8 +2344,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2224,6 +2364,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 @@ -2270,8 +2411,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2374,12 +2517,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 startProfileSession ()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 stopProfileSession ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2390,20 +2535,30 @@ public abstract interface class io/sentry/Sentry$OptionsConfiguration { public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSampleRate ()Ljava/lang/Double; public fun getProfilingTracesDirPath ()Ljava/lang/String; public fun getProfilingTracesHz ()I public fun getTraceSampleRate ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; + public fun isContinuousProfileSampled ()Z + public fun isContinuousProfilingEnabled ()Z + public fun isEnableAppStartProfiling ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setContinuousProfileSampled (Z)V + public fun setContinuousProfilingEnabled (Z)V + public fun setEnableAppStartProfiling (Z)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V + public fun setStartProfilerOnAppStart (Z)V public fun setTraceSampleRate (Ljava/lang/Double;)V public fun setTraceSampled (Z)V public fun setUnknown (Ljava/util/Map;)V @@ -2416,7 +2571,12 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; + public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_ENABLE_APP_START_PROFILING Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_START_PROFILER_ON_APP_START Ljava/lang/String; + public static final field PROFILE_LIFECYCLE Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; public static final field PROFILING_TRACES_DIR_PATH Ljava/lang/String; @@ -2509,6 +2669,7 @@ 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 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; @@ -2588,6 +2749,7 @@ 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 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; @@ -2719,6 +2881,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field Event Lio/sentry/SentryItemType; public static final field Feedback 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; @@ -2856,9 +3019,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; @@ -2902,6 +3067,8 @@ public class io/sentry/SentryOptions { public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; public fun getProfilingTracesDirPath ()Ljava/lang/String; @@ -2929,7 +3096,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; @@ -2938,6 +3104,7 @@ public class io/sentry/SentryOptions { public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z public fun isCaptureOpenTelemetryEvents ()Z + public fun isContinuousProfilingEnabled ()Z public fun isDebug ()Z public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z @@ -2961,6 +3128,7 @@ public class io/sentry/SentryOptions { public fun isSendClientReports ()Z public fun isSendDefaultPii ()Z public fun isSendModules ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z @@ -2976,8 +3144,10 @@ public class io/sentry/SentryOptions { public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V public fun setCaptureOpenTelemetryEvents (Z)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 @@ -3060,7 +3230,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 @@ -3555,6 +3724,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; } @@ -3690,6 +3860,7 @@ public final class io/sentry/TraceContext$JsonKeys { public final class io/sentry/TracesSampler { public fun (Lio/sentry/SentryOptions;)V public fun sample (Lio/sentry/SamplingContext;)Lio/sentry/TracesSamplingDecision; + public fun sampleSessionProfile (D)Z } public final class io/sentry/TracesSamplingDecision { @@ -3750,14 +3921,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; @@ -4371,9 +4534,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 @@ -4389,6 +4553,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 } @@ -4492,6 +4657,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 @@ -4511,6 +4677,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 setSpring (Lio/sentry/protocol/Spring;)V @@ -4574,6 +4741,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; @@ -6377,6 +6545,7 @@ public final class io/sentry/util/SampleRateUtils { public fun ()V public static fun backfilledSampleRand (Lio/sentry/TracesSamplingDecision;)Lio/sentry/TracesSamplingDecision; public static fun backfilledSampleRand (Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)Ljava/lang/Double; + public static fun isValidContinuousProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z @@ -6461,6 +6630,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 @@ -6470,6 +6640,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 @@ -6479,6 +6650,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/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index d2e11ce2d7..f78624bfe3 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -1,7 +1,6 @@ package io.sentry; import io.sentry.protocol.SdkVersion; -import org.jetbrains.annotations.Nullable; import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -14,7 +13,6 @@ *

Beware that experimental options can change at any time. */ public final class ExperimentalOptions { - private @NotNull SentryReplayOptions sessionReplay; /** * Indicates the percentage in which the profiles for the session will be created. Specifying 0 diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index 3c16504eb7..ccacccf9c0 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.util.SentryRandom; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -50,7 +51,10 @@ public SentryAppStartProfilingOptions() { traceSampleRate = samplingDecision.getSampleRate(); profileSampled = samplingDecision.getProfileSampled(); profileSampleRate = samplingDecision.getProfileSampleRate(); - continuousProfileSampled = options.getInternalTracesSampler().sampleSessionProfile(); + continuousProfileSampled = + options + .getInternalTracesSampler() + .sampleSessionProfile(SentryRandom.current().nextDouble()); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 793b20a4ad..18ef57fe89 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -9,7 +9,6 @@ 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; import java.util.Map; diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index ba3d98cafb..5430b9242a 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -82,10 +82,9 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont return new TracesSamplingDecision(false, null, sampleRand, false, null); } - public boolean sampleSessionProfile() { + public boolean sampleSessionProfile(final double sampleRand) { final @Nullable Double sampling = options.getProfileSessionSampleRate(); - return sampling != null && sample(sampling); - return !(aDouble < getRandom().nextDouble()); + return sampling != null && sample(sampling, sampleRand); } private boolean sample(final @NotNull Double sampleRate, final @NotNull Double sampleRand) { diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index 4110dc55e7..31c07b16de 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -2,6 +2,7 @@ package io.sentry import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -54,6 +55,6 @@ class SpanContextTest { trace.setData("k", "v") trace.setData("k", null) trace.setData(null, null) - assertTrue(trace.data.isEmpty()) + assertFalse(trace.data.containsKey("k")) } } diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 1264058c9e..99718735a1 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.util.SentryRandom import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -165,23 +166,24 @@ class TracesSamplerTest { } @Test - fun `when profileSessionSampleRate is not set returns true`() { - val sampler = fixture.getSut(randomResult = 1.0) - val sampled = sampler.sampleSessionProfile() + fun `when profileSessionSampleRate is not set returns false`() { + val sampler = fixture.getSut() + val sampled = sampler.sampleSessionProfile(1.0) assertFalse(sampled) } @Test fun `when profileSessionSampleRate is set and random returns lower number returns true`() { - val sampler = fixture.getSut(randomResult = 0.1, profileSessionSampleRate = 0.2) - val sampled = sampler.sampleSessionProfile() + SentryRandom.current().nextDouble() + val sampler = fixture.getSut(profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile(0.1) assertTrue(sampled) } @Test fun `when profileSessionSampleRate is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, profileSessionSampleRate = 0.2) - val sampled = sampler.sampleSessionProfile() + val sampler = fixture.getSut(profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile(0.9) assertFalse(sampled) } From f467dbe0368f92c228c3acf08915094b149400f3 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 18 Mar 2025 13:11:43 +0100 Subject: [PATCH 13/14] merged main and updated tests --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1e172b89f5..38d67ceb10 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Mon Mar 17 13:40:54 CET 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 9954e1c41d7eaa6ddfbeb7bdafb39c2bbc14dcf4 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 18 Mar 2025 14:03:07 +0100 Subject: [PATCH 14/14] merged main and updated tests --- .../spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c7b732f460..b5bd831c88 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(3)).options + verify(fixture.scopes, times(4)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any())