diff --git a/CHANGELOG.md b/CHANGELOG.md index 6accb56005..9e1561ebfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Performance + +- Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819)) + ## 8.50.0 ### Android 17 support diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java index 3d4cedb1b5..de1c40c570 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java @@ -8,9 +8,7 @@ import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.AutoClosableReentrantLock; -import io.sentry.util.LazyEvaluator; -import java.util.Timer; -import java.util.TimerTask; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -22,9 +20,8 @@ final class LifecycleWatcher implements AppState.AppStateListener { private final long sessionIntervalMillis; - private @Nullable TimerTask timerTask; - private final @NotNull LazyEvaluator timer = new LazyEvaluator<>(() -> new Timer(true)); - private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); + private @Nullable Future endSessionFuture; + private final @NotNull AutoClosableReentrantLock endSessionLock = new AutoClosableReentrantLock(); private final @NotNull IScopes scopes; private final boolean enableSessionTracking; private final boolean enableAppLifecycleBreadcrumbs; @@ -104,29 +101,40 @@ public void onBackground() { } private void scheduleEndSession() { - try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { + try (final @NotNull ISentryLifecycleToken ignored = endSessionLock.acquire()) { cancelTask(); - timerTask = - new TimerTask() { - @Override - public void run() { - if (enableSessionTracking) { - scopes.endSession(); - } - scopes.getOptions().getReplayController().stop(); - scopes.getOptions().getContinuousProfiler().close(false); + final @NotNull Runnable endSession = + () -> { + if (enableSessionTracking) { + scopes.endSession(); } + scopes.getOptions().getReplayController().stop(); + scopes.getOptions().getContinuousProfiler().close(false); }; - timer.getValue().schedule(timerTask, sessionIntervalMillis); + try { + endSessionFuture = + scopes + .getOptions() + .getTimerExecutorService() + .schedule(endSession, sessionIntervalMillis); + } catch (Throwable e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to schedule end of session. Ending it now.", e); + // if we cannot re-check after the session interval, end the session right away instead of + // leaving it open forever + endSession.run(); + } } } private void cancelTask() { - try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { - if (timerTask != null) { - timerTask.cancel(); - timerTask = null; + try (final @NotNull ISentryLifecycleToken ignored = endSessionLock.acquire()) { + if (endSessionFuture != null) { + endSessionFuture.cancel(false); + endSessionFuture = null; } } } @@ -144,13 +152,7 @@ private void addAppBreadcrumb(final @NotNull String state) { @TestOnly @Nullable - TimerTask getTimerTask() { - return timerTask; - } - - @TestOnly - @NotNull - Timer getTimer() { - return timer.getValue(); + Future getEndSessionFuture() { + return endSessionFuture; } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt index 09c4fae8dc..ce518eabb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt @@ -7,6 +7,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.ReplayController import io.sentry.ScopeCallback +import io.sentry.SentryExecutorService import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.Session @@ -32,7 +33,8 @@ class LifecycleWatcherTest { private class Fixture { val scopes = mock() val dateProvider = mock() - val options = SentryOptions() + // a real executor so scheduled end-session tasks actually run + val options = SentryOptions().apply { setTimerExecutorService(SentryExecutorService(this)) } val replayController = mock() val continuousProfiler = mock() @@ -115,10 +117,10 @@ class LifecycleWatcherTest { watcher.onForeground() watcher.onBackground() - assertNotNull(watcher.timerTask) + assertNotNull(watcher.endSessionFuture) watcher.onForeground() - assertNull(watcher.timerTask) + assertNull(watcher.endSessionFuture) verify(fixture.scopes, never()).endSession() verify(fixture.replayController, never()).stop() @@ -186,13 +188,6 @@ class LifecycleWatcherTest { verify(fixture.scopes, never()).addBreadcrumb(any()) } - @Test - fun `timer is created if session tracking is enabled`() { - val watcher = - fixture.getSUT(enableAutoSessionTracking = true, enableAppLifecycleBreadcrumbs = false) - assertNotNull(watcher.timer) - } - @Test fun `if the scopes has already a fresh session running, don't start new one`() { val watcher =