From d7d846f897dede84272ac5260ba105440d0d962f Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 9 Sep 2024 18:07:53 +0200 Subject: [PATCH] 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 478a1ddd3c..a3434cd8f5 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/IHub;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 d9ece7fb46..40013c2336 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 ceab2fc326..bc4e522708 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -557,6 +557,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; } @@ -1188,6 +1196,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() +}