Skip to content

Commit 023ef00

Browse files
runningcodeclaude
andauthored
perf: Schedule session end on shared executor (JAVA-653) (#5819)
* perf: Schedule session end on shared executor (JAVA-653) LifecycleWatcher created a java.util.Timer whose thread was spawned on the first background transition and lived for the rest of the process. Schedule the session-end task on the shared timer executor instead, whose single worker thread is reused and self-terminates when idle. If scheduling fails (executor already shut down), the session is ended right away instead of leaking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f210d11 commit 023ef00

3 files changed

Lines changed: 42 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Performance
6+
7+
- 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+
39
## 8.50.0
410

511
### Android 17 support

sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
import io.sentry.transport.CurrentDateProvider;
99
import io.sentry.transport.ICurrentDateProvider;
1010
import io.sentry.util.AutoClosableReentrantLock;
11-
import io.sentry.util.LazyEvaluator;
12-
import java.util.Timer;
13-
import java.util.TimerTask;
11+
import java.util.concurrent.Future;
1412
import java.util.concurrent.atomic.AtomicLong;
1513
import org.jetbrains.annotations.NotNull;
1614
import org.jetbrains.annotations.Nullable;
@@ -22,9 +20,8 @@ final class LifecycleWatcher implements AppState.AppStateListener {
2220

2321
private final long sessionIntervalMillis;
2422

25-
private @Nullable TimerTask timerTask;
26-
private final @NotNull LazyEvaluator<Timer> timer = new LazyEvaluator<>(() -> new Timer(true));
27-
private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock();
23+
private @Nullable Future<?> endSessionFuture;
24+
private final @NotNull AutoClosableReentrantLock endSessionLock = new AutoClosableReentrantLock();
2825
private final @NotNull IScopes scopes;
2926
private final boolean enableSessionTracking;
3027
private final boolean enableAppLifecycleBreadcrumbs;
@@ -104,29 +101,40 @@ public void onBackground() {
104101
}
105102

106103
private void scheduleEndSession() {
107-
try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) {
104+
try (final @NotNull ISentryLifecycleToken ignored = endSessionLock.acquire()) {
108105
cancelTask();
109-
timerTask =
110-
new TimerTask() {
111-
@Override
112-
public void run() {
113-
if (enableSessionTracking) {
114-
scopes.endSession();
115-
}
116-
scopes.getOptions().getReplayController().stop();
117-
scopes.getOptions().getContinuousProfiler().close(false);
106+
final @NotNull Runnable endSession =
107+
() -> {
108+
if (enableSessionTracking) {
109+
scopes.endSession();
118110
}
111+
scopes.getOptions().getReplayController().stop();
112+
scopes.getOptions().getContinuousProfiler().close(false);
119113
};
120114

121-
timer.getValue().schedule(timerTask, sessionIntervalMillis);
115+
try {
116+
endSessionFuture =
117+
scopes
118+
.getOptions()
119+
.getTimerExecutorService()
120+
.schedule(endSession, sessionIntervalMillis);
121+
} catch (Throwable e) {
122+
scopes
123+
.getOptions()
124+
.getLogger()
125+
.log(SentryLevel.WARNING, "Failed to schedule end of session. Ending it now.", e);
126+
// if we cannot re-check after the session interval, end the session right away instead of
127+
// leaving it open forever
128+
endSession.run();
129+
}
122130
}
123131
}
124132

125133
private void cancelTask() {
126-
try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) {
127-
if (timerTask != null) {
128-
timerTask.cancel();
129-
timerTask = null;
134+
try (final @NotNull ISentryLifecycleToken ignored = endSessionLock.acquire()) {
135+
if (endSessionFuture != null) {
136+
endSessionFuture.cancel(false);
137+
endSessionFuture = null;
130138
}
131139
}
132140
}
@@ -144,13 +152,7 @@ private void addAppBreadcrumb(final @NotNull String state) {
144152

145153
@TestOnly
146154
@Nullable
147-
TimerTask getTimerTask() {
148-
return timerTask;
149-
}
150-
151-
@TestOnly
152-
@NotNull
153-
Timer getTimer() {
154-
return timer.getValue();
155+
Future<?> getEndSessionFuture() {
156+
return endSessionFuture;
155157
}
156158
}

sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.sentry.IScope
77
import io.sentry.IScopes
88
import io.sentry.ReplayController
99
import io.sentry.ScopeCallback
10+
import io.sentry.SentryExecutorService
1011
import io.sentry.SentryLevel
1112
import io.sentry.SentryOptions
1213
import io.sentry.Session
@@ -32,7 +33,8 @@ class LifecycleWatcherTest {
3233
private class Fixture {
3334
val scopes = mock<IScopes>()
3435
val dateProvider = mock<ICurrentDateProvider>()
35-
val options = SentryOptions()
36+
// a real executor so scheduled end-session tasks actually run
37+
val options = SentryOptions().apply { setTimerExecutorService(SentryExecutorService(this)) }
3638
val replayController = mock<ReplayController>()
3739
val continuousProfiler = mock<IContinuousProfiler>()
3840

@@ -115,10 +117,10 @@ class LifecycleWatcherTest {
115117
watcher.onForeground()
116118

117119
watcher.onBackground()
118-
assertNotNull(watcher.timerTask)
120+
assertNotNull(watcher.endSessionFuture)
119121

120122
watcher.onForeground()
121-
assertNull(watcher.timerTask)
123+
assertNull(watcher.endSessionFuture)
122124

123125
verify(fixture.scopes, never()).endSession()
124126
verify(fixture.replayController, never()).stop()
@@ -186,13 +188,6 @@ class LifecycleWatcherTest {
186188
verify(fixture.scopes, never()).addBreadcrumb(any<Breadcrumb>())
187189
}
188190

189-
@Test
190-
fun `timer is created if session tracking is enabled`() {
191-
val watcher =
192-
fixture.getSUT(enableAutoSessionTracking = true, enableAppLifecycleBreadcrumbs = false)
193-
assertNotNull(watcher.timer)
194-
}
195-
196191
@Test
197192
fun `if the scopes has already a fresh session running, don't start new one`() {
198193
val watcher =

0 commit comments

Comments
 (0)