Skip to content

Commit 2c7271a

Browse files
romtsnadinauer
authored andcommitted
perf(executor): Prewarm SentryExecutorService (#4606)
1 parent 4425a1b commit 2c7271a

23 files changed

+357
-82
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542))
88
- Use single `LifecycleObserver` and multi-cast it to the integrations interested in lifecycle states ([#4567](https://github.com/getsentry/sentry-java/pull/4567))
9+
- Prewarm `SentryExecutorService` for better performance at runtime ([#4606](https://github.com/getsentry/sentry-java/pull/4606))
910

1011
### Fixes
1112

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.content.pm.PackageInfo;
1616
import android.content.pm.PackageManager;
1717
import android.os.Build;
18+
import android.os.Handler;
1819
import android.util.DisplayMetrics;
1920
import io.sentry.ILogger;
2021
import io.sentry.SentryLevel;
@@ -455,8 +456,10 @@ public static boolean appIsLibraryForComposePreview(final @NotNull Context conte
455456
final @NotNull Context context,
456457
final @NotNull SentryOptions options,
457458
final @Nullable BroadcastReceiver receiver,
458-
final @NotNull IntentFilter filter) {
459-
return registerReceiver(context, new BuildInfoProvider(options.getLogger()), receiver, filter);
459+
final @NotNull IntentFilter filter,
460+
final @Nullable Handler handler) {
461+
return registerReceiver(
462+
context, new BuildInfoProvider(options.getLogger()), receiver, filter, handler);
460463
}
461464

462465
/** Register an exported BroadcastReceiver, independently from platform version. */
@@ -465,15 +468,17 @@ public static boolean appIsLibraryForComposePreview(final @NotNull Context conte
465468
final @NotNull Context context,
466469
final @NotNull BuildInfoProvider buildInfoProvider,
467470
final @Nullable BroadcastReceiver receiver,
468-
final @NotNull IntentFilter filter) {
471+
final @NotNull IntentFilter filter,
472+
final @Nullable Handler handler) {
469473
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
470474
// From https://developer.android.com/guide/components/broadcasts#context-registered-receivers
471475
// If this receiver is listening for broadcasts sent from the system or from other apps, even
472476
// other apps that you own—use the RECEIVER_EXPORTED flag. If instead this receiver is
473477
// listening only for broadcasts sent by your app, use the RECEIVER_NOT_EXPORTED flag.
474-
return context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
478+
return context.registerReceiver(
479+
receiver, filter, null, handler, Context.RECEIVER_NOT_EXPORTED);
475480
} else {
476-
return context.registerReceiver(receiver, filter);
481+
return context.registerReceiver(receiver, filter, null, handler);
477482
}
478483
}
479484

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ private Date getBootTime() {
275275
@Nullable
276276
private Intent getBatteryIntent() {
277277
return ContextUtils.registerReceiver(
278-
context, buildInfoProvider, null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
278+
context, buildInfoProvider, null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), null);
279279
}
280280

281281
/**

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +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;
1112
import java.util.Timer;
1213
import java.util.TimerTask;
1314
import java.util.concurrent.atomic.AtomicLong;
@@ -22,7 +23,7 @@ final class LifecycleWatcher implements AppState.AppStateListener {
2223
private final long sessionIntervalMillis;
2324

2425
private @Nullable TimerTask timerTask;
25-
private final @NotNull Timer timer = new Timer(true);
26+
private final @NotNull LazyEvaluator<Timer> timer = new LazyEvaluator<>(() -> new Timer(true));
2627
private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock();
2728
private final @NotNull IScopes scopes;
2829
private final boolean enableSessionTracking;
@@ -105,21 +106,19 @@ public void onBackground() {
105106
private void scheduleEndSession() {
106107
try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) {
107108
cancelTask();
108-
if (timer != null) {
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);
109+
timerTask =
110+
new TimerTask() {
111+
@Override
112+
public void run() {
113+
if (enableSessionTracking) {
114+
scopes.endSession();
118115
}
119-
};
116+
scopes.getOptions().getReplayController().stop();
117+
scopes.getOptions().getContinuousProfiler().close(false);
118+
}
119+
};
120120

121-
timer.schedule(timerTask, sessionIntervalMillis);
122-
}
121+
timer.getValue().schedule(timerTask, sessionIntervalMillis);
123122
}
124123
}
125124

@@ -152,6 +151,6 @@ TimerTask getTimerTask() {
152151
@TestOnly
153152
@NotNull
154153
Timer getTimer() {
155-
return timer;
154+
return timer.getValue();
156155
}
157156
}

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import android.content.Intent;
2626
import android.content.IntentFilter;
2727
import android.os.Bundle;
28+
import android.os.Handler;
29+
import android.os.HandlerThread;
30+
import android.os.Process;
2831
import io.sentry.Breadcrumb;
2932
import io.sentry.Hint;
3033
import io.sentry.IScopes;
@@ -63,6 +66,7 @@ public final class SystemEventsBreadcrumbsIntegration
6366
private volatile boolean isClosed = false;
6467
private volatile boolean isStopped = false;
6568
private volatile IntentFilter filter = null;
69+
private volatile HandlerThread handlerThread = null;
6670
private final @NotNull AtomicBoolean isReceiverRegistered = new AtomicBoolean(false);
6771
private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock();
6872
// Track previous battery state to avoid duplicate breadcrumbs when values haven't changed
@@ -138,10 +142,19 @@ private void registerReceiver(
138142
filter.addAction(item);
139143
}
140144
}
145+
if (handlerThread == null) {
146+
handlerThread =
147+
new HandlerThread(
148+
"SystemEventsReceiver", Process.THREAD_PRIORITY_BACKGROUND);
149+
handlerThread.start();
150+
}
141151
try {
142152
// registerReceiver can throw SecurityException but it's not documented in the
143153
// official docs
144-
ContextUtils.registerReceiver(context, options, receiver, filter);
154+
155+
// onReceive will be called on this handler thread
156+
final @NotNull Handler handler = new Handler(handlerThread.getLooper());
157+
ContextUtils.registerReceiver(context, options, receiver, filter, handler);
145158
if (!isReceiverRegistered.getAndSet(true)) {
146159
options
147160
.getLogger()
@@ -195,6 +208,10 @@ public void close() throws IOException {
195208
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
196209
isClosed = true;
197210
filter = null;
211+
if (handlerThread != null) {
212+
handlerThread.quit();
213+
}
214+
handlerThread = null;
198215
}
199216

200217
AppState.getInstance().removeAppStateListener(this);
@@ -293,25 +310,15 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
293310

294311
final BatteryState state = batteryState;
295312
final long now = System.currentTimeMillis();
296-
try {
297-
options
298-
.getExecutorService()
299-
.submit(
300-
() -> {
301-
final Breadcrumb breadcrumb = createBreadcrumb(now, intent, action, state);
302-
final Hint hint = new Hint();
303-
hint.set(ANDROID_INTENT, intent);
304-
scopes.addBreadcrumb(breadcrumb, hint);
305-
});
306-
} catch (Throwable t) {
307-
// ignored
308-
}
313+
final Breadcrumb breadcrumb = createBreadcrumb(now, intent, action, state);
314+
final Hint hint = new Hint();
315+
hint.set(ANDROID_INTENT, intent);
316+
scopes.addBreadcrumb(breadcrumb, hint);
309317
}
310318

311319
// in theory this should be ThreadLocal, but we won't have more than 1 thread accessing it,
312320
// so we save some memory here and CPU cycles. 64 is because all intent actions we subscribe for
313321
// are less than 64 chars. We also don't care about encoding as those are always UTF.
314-
// TODO: _MULTI_THREADED_EXECUTOR_
315322
private final char[] buf = new char[64];
316323

317324
@TestOnly
@@ -365,8 +372,8 @@ String getStringAfterDotFast(final @Nullable String str) {
365372
}
366373
} else {
367374
final Bundle extras = intent.getExtras();
368-
final Map<String, String> newExtras = new HashMap<>();
369375
if (extras != null && !extras.isEmpty()) {
376+
final Map<String, String> newExtras = new HashMap<>(extras.size());
370377
for (String item : extras.keySet()) {
371378
try {
372379
@SuppressWarnings("deprecation")

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class AndroidProfilerTest {
8080
override fun close(timeoutMillis: Long) {}
8181

8282
override fun isClosed() = false
83+
84+
override fun prewarm() = Unit
8385
}
8486

8587
val options =

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class AndroidTransactionProfilerTest {
8989
override fun close(timeoutMillis: Long) {}
9090

9191
override fun isClosed() = false
92+
93+
override fun prewarm() = Unit
9294
}
9395

9496
val options =

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import kotlin.test.assertTrue
3030
import org.junit.runner.RunWith
3131
import org.mockito.kotlin.any
3232
import org.mockito.kotlin.eq
33+
import org.mockito.kotlin.isNull
3334
import org.mockito.kotlin.mock
3435
import org.mockito.kotlin.spy
3536
import org.mockito.kotlin.verify
@@ -221,8 +222,8 @@ class ContextUtilsTest {
221222
val filter = mock<IntentFilter>()
222223
val context = mock<Context>()
223224
whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.S)
224-
ContextUtils.registerReceiver(context, buildInfo, receiver, filter)
225-
verify(context).registerReceiver(eq(receiver), eq(filter))
225+
ContextUtils.registerReceiver(context, buildInfo, receiver, filter, null)
226+
verify(context).registerReceiver(eq(receiver), eq(filter), isNull(), isNull())
226227
}
227228

228229
@Test
@@ -232,8 +233,15 @@ class ContextUtilsTest {
232233
val filter = mock<IntentFilter>()
233234
val context = mock<Context>()
234235
whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.TIRAMISU)
235-
ContextUtils.registerReceiver(context, buildInfo, receiver, filter)
236-
verify(context).registerReceiver(eq(receiver), eq(filter), eq(Context.RECEIVER_NOT_EXPORTED))
236+
ContextUtils.registerReceiver(context, buildInfo, receiver, filter, null)
237+
verify(context)
238+
.registerReceiver(
239+
eq(receiver),
240+
eq(filter),
241+
isNull(),
242+
isNull(),
243+
eq(Context.RECEIVER_NOT_EXPORTED),
244+
)
237245
}
238246

239247
@Test

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class SystemEventsBreadcrumbsIntegrationTest {
8989

9090
sut.register(fixture.scopes, fixture.options)
9191

92-
verify(fixture.context).registerReceiver(any(), any(), any())
92+
verify(fixture.context).registerReceiver(any(), any(), anyOrNull(), anyOrNull(), any())
9393
assertNotNull(sut.receiver)
9494
}
9595

@@ -299,7 +299,8 @@ class SystemEventsBreadcrumbsIntegrationTest {
299299
@Test
300300
fun `Do not crash if registerReceiver throws exception`() {
301301
val sut = fixture.getSut()
302-
whenever(fixture.context.registerReceiver(any(), any(), any())).thenThrow(SecurityException())
302+
whenever(fixture.context.registerReceiver(any(), any(), anyOrNull(), anyOrNull(), any()))
303+
.thenThrow(SecurityException())
303304

304305
sut.register(fixture.scopes, fixture.options)
305306

@@ -448,12 +449,13 @@ class SystemEventsBreadcrumbsIntegrationTest {
448449
val sut = fixture.getSut()
449450

450451
sut.register(fixture.scopes, fixture.options)
451-
verify(fixture.context).registerReceiver(any(), any(), any())
452+
verify(fixture.context).registerReceiver(any(), any(), anyOrNull(), anyOrNull(), any())
452453

453454
sut.onBackground()
454455
sut.onForeground()
455456

456-
verify(fixture.context, times(2)).registerReceiver(any(), any(), any())
457+
verify(fixture.context, times(2))
458+
.registerReceiver(any(), any(), anyOrNull(), anyOrNull(), any())
457459
assertNotNull(sut.receiver)
458460
}
459461

@@ -462,7 +464,7 @@ class SystemEventsBreadcrumbsIntegrationTest {
462464
val sut = fixture.getSut()
463465

464466
sut.register(fixture.scopes, fixture.options)
465-
verify(fixture.context).registerReceiver(any(), any(), any())
467+
verify(fixture.context).registerReceiver(any(), any(), anyOrNull(), anyOrNull(), any())
466468
val receiver = sut.receiver
467469

468470
sut.onForeground()

sentry-test-support/api/sentry-test-support.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class io/sentry/test/DeferredExecutorService : io/sentry/ISentryExe
2020
public fun close (J)V
2121
public final fun hasScheduledRunnables ()Z
2222
public fun isClosed ()Z
23+
public fun prewarm ()V
2324
public final fun runAll ()V
2425
public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future;
2526
public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future;
@@ -30,6 +31,7 @@ public final class io/sentry/test/ImmediateExecutorService : io/sentry/ISentryEx
3031
public fun <init> ()V
3132
public fun close (J)V
3233
public fun isClosed ()Z
34+
public fun prewarm ()V
3335
public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future;
3436
public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future;
3537
public fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future;

0 commit comments

Comments
 (0)