Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,9 +20,8 @@ final class LifecycleWatcher implements AppState.AppStateListener {

private final long sessionIntervalMillis;

private @Nullable TimerTask timerTask;
private final @NotNull LazyEvaluator<Timer> 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;
Expand Down Expand Up @@ -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;
}
}
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +33,8 @@ class LifecycleWatcherTest {
private class Fixture {
val scopes = mock<IScopes>()
val dateProvider = mock<ICurrentDateProvider>()
val options = SentryOptions()
// a real executor so scheduled end-session tasks actually run
val options = SentryOptions().apply { setTimerExecutorService(SentryExecutorService(this)) }
val replayController = mock<ReplayController>()
val continuousProfiler = mock<IContinuousProfiler>()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -186,13 +188,6 @@ class LifecycleWatcherTest {
verify(fixture.scopes, never()).addBreadcrumb(any<Breadcrumb>())
}

@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 =
Expand Down
Loading