From 52625d321ad286241c32d02ff0cc8f7d36a27d37 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 17:54:56 +0200 Subject: [PATCH 1/4] perf(android): Parse app start profiling config without JsonSerializer (JAVA-621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SentryPerformanceProvider.launchAppStartProfiler ran in ContentProvider.onCreate — main thread, before Application.onCreate, on every cold start — and built a full JsonSerializer(SentryOptions.empty()) to read one small config file. That path only ever reads options.getLogger(), but JsonSerializer's constructor registers every known deserializer to use exactly one of them. Calling the deserializer directly cuts the parse from 221 to 33 allocations and ~7.5us to ~3.7us (Pixel 10, androidx Microbenchmark, allocationCount is deterministic). Malformed input still yields null so callers keep reporting it as a deserialization failure rather than a read error. Co-Authored-By: Claude Opus 5 (1M context) --- .../core/SentryPerformanceProvider.java | 24 ++++++++++++++++--- .../core/SentryPerformanceProviderTest.kt | 15 ++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index c37d384aeec..4e294284b11 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -13,7 +13,7 @@ import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; -import io.sentry.JsonSerializer; +import io.sentry.JsonObjectReader; import io.sentry.SentryAppStartProfilingOptions; import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; @@ -117,8 +117,7 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri try (final @NotNull Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(configFile)))) { final @Nullable SentryAppStartProfilingOptions profilingOptions = - new JsonSerializer(SentryOptions.empty()) - .deserialize(reader, SentryAppStartProfilingOptions.class); + deserializeProfilingConfig(reader); if (profilingOptions == null) { logger.log( @@ -166,6 +165,25 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri } } + /** + * Parses the app start profiling config with only the deserializer it needs. Going through {@link + * io.sentry.JsonSerializer} would allocate a full {@link SentryOptions} plus every registered + * deserializer on the main thread before {@code Application.onCreate}, to use exactly one of + * them. + * + *

Returns null on malformed input, matching what {@code JsonSerializer.deserialize} did, so + * callers keep reporting it as a deserialization failure rather than a read error. + */ + private @Nullable SentryAppStartProfilingOptions deserializeProfilingConfig( + final @NotNull Reader reader) { + try (final @NotNull JsonObjectReader jsonReader = new JsonObjectReader(reader)) { + return new SentryAppStartProfilingOptions.Deserializer().deserialize(jsonReader, logger); + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error when deserializing", e); + return null; + } + } + private void createAndStartContinuousProfiler( final @NotNull Context context, final @NotNull SentryAppStartProfilingOptions profilingOptions, diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 254571181a6..bff6cdfad37 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -153,6 +153,21 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when config file is malformed, profiler is not started`() { + fixture.getSut { config -> config.writeText("{\"profile_sampled\": tru") } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("Error when deserializing"), any()) + verify(fixture.logger) + .log( + eq(SentryLevel.WARNING), + eq( + "Unable to deserialize the SentryAppStartProfilingOptions. App start profiling will not start." + ), + ) + } + @Test fun `when profiling is disabled, profiler is not started`() { fixture.getSut { config -> From eef22e7fb0f682eff6d6287b89719bae00c22b63 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 29 Jul 2026 17:56:08 +0200 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1823146d55a..b52621fae5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Performance - Remove an unused lock from `SentryPerformanceProvider`, which was allocated on every cold start in `ContentProvider.onCreate` without ever being acquired ([#5871](https://github.com/getsentry/sentry-java/pull/5871)) +- Parse the app start profiling config with only the deserializer it needs instead of building a full `JsonSerializer` and `SentryOptions`, cutting 188 of 221 allocations on the main thread before `Application.onCreate` ([#5867](https://github.com/getsentry/sentry-java/pull/5867)) ## 8.51.0 From 8e27d0c11a480286246b69a9ea2b3d1debc7c222 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 30 Jul 2026 10:26:12 +0200 Subject: [PATCH 3/4] ref(android): Catch only expected exceptions when parsing profiling config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrow the deserialization catch from Throwable to Exception. The vendored JSON reader signals bad input with IOException (MalformedJsonException, EOFException), IllegalStateException on token type mismatch, and NumberFormatException on an unparseable number — all Exception subclasses. Catching Throwable additionally swallowed Error, which is never a recoverable "config file is bad" signal. This also restores parity with JsonSerializer.deserialize, which catches Exception, so the replaced behaviour is matched exactly rather than widened. Co-Authored-By: Claude Opus 5 (1M context) --- .../io/sentry/android/core/SentryPerformanceProvider.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 4e294284b11..6e4732c4bb0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -172,13 +172,17 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri * them. * *

Returns null on malformed input, matching what {@code JsonSerializer.deserialize} did, so - * callers keep reporting it as a deserialization failure rather than a read error. + * callers keep reporting it as a deserialization failure rather than a read error. The vendored + * JSON reader signals bad input with {@link java.io.IOException} (including {@code + * MalformedJsonException} and {@code EOFException}), {@link IllegalStateException} on a token + * type mismatch, and {@link NumberFormatException} on an unparseable number — all {@link + * Exception} subclasses, so {@code Error} propagates instead of being swallowed here. */ private @Nullable SentryAppStartProfilingOptions deserializeProfilingConfig( final @NotNull Reader reader) { try (final @NotNull JsonObjectReader jsonReader = new JsonObjectReader(reader)) { return new SentryAppStartProfilingOptions.Deserializer().deserialize(jsonReader, logger); - } catch (Throwable e) { + } catch (Exception e) { logger.log(SentryLevel.ERROR, "Error when deserializing", e); return null; } From b296f38727108efa86317d12e255f62dbeaed999 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 30 Jul 2026 11:46:34 +0200 Subject: [PATCH 4/4] ref(android): Trim javadoc on deserializeProfilingConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the paragraph enumerating which exception types the vendored JSON reader throws. That catch (Exception) does not swallow Error is a language-level given, and listing the reader's internal exception types invites the comment to drift as that code changes. The rationale a reader cannot infer from the code — why the deserializer is called directly, and why null is returned instead of rethrowing — stays. Co-Authored-By: Claude Opus 5 (1M context) --- .../io/sentry/android/core/SentryPerformanceProvider.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 6e4732c4bb0..9d1f2e13ec4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -172,11 +172,7 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri * them. * *

Returns null on malformed input, matching what {@code JsonSerializer.deserialize} did, so - * callers keep reporting it as a deserialization failure rather than a read error. The vendored - * JSON reader signals bad input with {@link java.io.IOException} (including {@code - * MalformedJsonException} and {@code EOFException}), {@link IllegalStateException} on a token - * type mismatch, and {@link NumberFormatException} on an unparseable number — all {@link - * Exception} subclasses, so {@code Error} propagates instead of being swallowed here. + * callers keep reporting it as a deserialization failure rather than a read error. */ private @Nullable SentryAppStartProfilingOptions deserializeProfilingConfig( final @NotNull Reader reader) {