From e8b898b3951e341ccb8f6c854c6e038dce29068b Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 2 Jul 2026 14:58:53 +0200 Subject: [PATCH] perf(core): Remove executor prewarm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SentryExecutorService is single-threaded, and prewarm() is submitted ahead of loadLazyFields() during init, so its 40-task schedule/cancel/purge loop cannot reduce first-task latency — it can only delay it. The thread creation and executor class loading it aimed to warm are paid identically by the first real task (loadLazyFields), which is submitted unconditionally right after, so prewarm warms nothing that would not already be warmed. On-device A/B benchmarks on a Galaxy A55 (Android 16) show no measurable first-useful-task speedup from prewarm and ~20us of extra background-thread work. Remove prewarm() from ISentryExecutorService and its implementations and from both init call sites. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 + .../android/core/AndroidProfilerTest.kt | 2 - .../core/AndroidTransactionProfilerTest.kt | 2 - .../src/main/kotlin/io/sentry/test/Mocks.kt | 6 --- sentry/api/sentry.api | 3 -- .../io/sentry/ISentryExecutorService.java | 6 --- .../io/sentry/NoOpSentryExecutorService.java | 3 -- sentry/src/main/java/io/sentry/Sentry.java | 1 - .../java/io/sentry/SentryExecutorService.java | 39 ------------------- .../main/java/io/sentry/SentryOptions.java | 1 - .../io/sentry/SentryExecutorServiceTest.kt | 16 -------- sentry/src/test/java/io/sentry/SentryTest.kt | 1 - 12 files changed, 2 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3505a359c36..107b916808a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ - Skip `Hint` allocation in `Scope.addBreadcrumb` when no `beforeBreadcrumb` callback is set ([#5689](https://github.com/getsentry/sentry-java/pull/5689)) - Speed up scope persistence by detecting the Sentry executor thread via a marker instead of a `Thread.getName()` name scan on every scope mutation ([#5691](https://github.com/getsentry/sentry-java/pull/5691)) +- Remove executor prewarm during SDK init ([#5681](https://github.com/getsentry/sentry-java/pull/5681)) + - The single-threaded `SentryExecutorService` queued the prewarm work ahead of the first useful task, so it could only delay init work, never speed it up; the thread and class loading it warmed are paid identically by the first real task submitted right after. ## 8.47.0 diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index 66bda9bce0b..f402af24f80 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -81,8 +81,6 @@ class AndroidProfilerTest { override fun close(timeoutMillis: Long) {} override fun isClosed() = false - - override fun prewarm() = Unit } val options = diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 0829e4dc796..b37a6bbdee5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -89,8 +89,6 @@ class AndroidTransactionProfilerTest { override fun close(timeoutMillis: Long) {} override fun isClosed() = false - - override fun prewarm() = Unit } val options = diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt index da69b7cf330..79076e9dc8f 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -35,8 +35,6 @@ class ImmediateExecutorService : ISentryExecutorService { override fun close(timeoutMillis: Long) {} override fun isClosed(): Boolean = false - - override fun prewarm() = Unit } class DeferredExecutorService : ISentryExecutorService { @@ -74,8 +72,6 @@ class DeferredExecutorService : ISentryExecutorService { override fun isClosed(): Boolean = false - override fun prewarm() = Unit - fun hasScheduledRunnables(): Boolean = scheduledRunnables.isNotEmpty() } @@ -90,8 +86,6 @@ class NonOverridableNoOpSentryExecutorService : ISentryExecutorService { override fun close(timeoutMillis: Long) {} override fun isClosed(): Boolean = false - - override fun prewarm() = Unit } fun createSentryClientMock(enabled: Boolean = true) = diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 5b37d89e848..09ede4804a3 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1134,7 +1134,6 @@ public abstract interface class io/sentry/ISentryClient { public abstract interface class io/sentry/ISentryExecutorService { public abstract fun close (J)V public abstract fun isClosed ()Z - public abstract fun prewarm ()V public abstract fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; public abstract fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; public abstract fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; @@ -1880,7 +1879,6 @@ public final class io/sentry/NoOpSentryExecutorService : io/sentry/ISentryExecut public fun close (J)V public static fun getInstance ()Lio/sentry/ISentryExecutorService; public fun isClosed ()Z - public fun prewarm ()V public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; public fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; @@ -3183,7 +3181,6 @@ public final class io/sentry/SentryExecutorService : io/sentry/ISentryExecutorSe public fun close (J)V public fun isClosed ()Z public static fun isSentryExecutorThread ()Z - public fun prewarm ()V public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; public fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; diff --git a/sentry/src/main/java/io/sentry/ISentryExecutorService.java b/sentry/src/main/java/io/sentry/ISentryExecutorService.java index ffad05361f6..9bdef8db2b7 100644 --- a/sentry/src/main/java/io/sentry/ISentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/ISentryExecutorService.java @@ -45,10 +45,4 @@ Future schedule(final @NotNull Runnable runnable, final long delayMillis) * @return If the executorService was previously closed */ boolean isClosed(); - - /** - * Pre-warms the executor service by increasing the initial queue capacity. SHOULD be called - * directly after instantiating this executor service. - */ - void prewarm(); } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java b/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java index c2ce81f6577..37778c3cc09 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java @@ -38,7 +38,4 @@ public void close(long timeoutMillis) {} public boolean isClosed() { return false; } - - @Override - public void prewarm() {} } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 919607e5879..f5397f191ad 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -351,7 +351,6 @@ private static void init(final @NotNull SentryOptions options, final boolean glo // to set a new one if (options.getExecutorService().isClosed()) { options.setExecutorService(new SentryExecutorService(options)); - options.getExecutorService().prewarm(); } // load lazy fields of the options in a separate thread diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index 3e24a1a5992..1936bb9c360 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -16,13 +16,6 @@ @ApiStatus.Internal public final class SentryExecutorService implements ISentryExecutorService { - /** - * ScheduledThreadPoolExecutor grows work queue by 50% each time. With the initial capacity of 16 - * it will have to resize 4 times to reach 40, which is a decent middle-ground for prewarming. - * This will prevent from growing in unexpected areas of the SDK. - */ - private static final int INITIAL_QUEUE_SIZE = 40; - /** * By default, the work queue is unbounded so it can grow as much as the memory allows. We want to * limit it by 271 which would be x8 times growth from the default initial capacity. @@ -32,9 +25,6 @@ public final class SentryExecutorService implements ISentryExecutorService { private final @NotNull ScheduledThreadPoolExecutor executorService; private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); - @SuppressWarnings("UnnecessaryLambda") - private final @NotNull Runnable dummyRunnable = () -> {}; - private final @Nullable SentryOptions options; @TestOnly @@ -120,35 +110,6 @@ public boolean isClosed() { } } - @SuppressWarnings({"FutureReturnValueIgnored"}) - @Override - public void prewarm() { - try { - executorService.submit( - () -> { - try { - // schedule a bunch of dummy runnables in the future that will never execute to - // trigger - // queue growth and then purge the queue - for (int i = 0; i < INITIAL_QUEUE_SIZE; i++) { - final Future future = - executorService.schedule(dummyRunnable, 365L, TimeUnit.DAYS); - future.cancel(true); - } - executorService.purge(); - } catch (RejectedExecutionException ignored) { - // ignore - } - }); - } catch (RejectedExecutionException e) { - if (options != null) { - options - .getLogger() - .log(SentryLevel.WARNING, "Prewarm task rejected from " + executorService, e); - } - } - } - /** * Whether the calling thread is one of the Sentry executor threads. Cheap replacement for * scanning {@code Thread.currentThread().getName()}, which is on hot paths (e.g. scope diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 0d038482d07..8c995001016 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -678,7 +678,6 @@ public void activate() { // SentryExecutorService should be initialized before any // SendCachedEventFireAndForgetIntegration executorService = new SentryExecutorService(this); - executorService.prewarm(); } // SpotlightIntegration is loaded via reflection to allow the sentry-spotlight module diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 2ccba650ad9..57dfb578ee9 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -1,6 +1,5 @@ package io.sentry -import io.sentry.test.getProperty import java.util.concurrent.BlockingQueue import java.util.concurrent.Callable import java.util.concurrent.CancellationException @@ -191,21 +190,6 @@ class SentryExecutorServiceTest { verify(executor).schedule(any(), any(), any()) } - @Test - fun `SentryExecutorService prewarm schedules dummy tasks and clears queue`() { - val executor = ScheduledThreadPoolExecutor(1) - - val sentryExecutor = SentryExecutorService(executor, null) - sentryExecutor.prewarm() - - Thread.sleep(1000) - - // the internal queue/array should be resized 4 times to 54 - assertEquals(54, (executor.queue.getProperty("queue") as Array<*>).size) - // the queue should be empty - assertEquals(0, executor.queue.size) - } - @Test fun `SentryExecutorService schedules any number of job`() { val executor = ScheduledThreadPoolExecutor(1) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 3712b083de7..4a122f11422 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -778,7 +778,6 @@ class SentryTest { it.cacheDirPath = getTempPath() it.setLogger(logger) it.executorService = SentryExecutorService() - it.executorService.prewarm() it.executorService.close(0) it.isDebug = true }