From a11693e462d297e47be329ea9b5e79cae7cb447c Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:30:56 +0200 Subject: [PATCH 1/2] collection: Reduce reflection cost during SDK init From 7a978c64dea0a3f9c30a6b1fdd78b963a37aaf63 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 30 Jun 2026 10:29:43 +0200 Subject: [PATCH 2/2] perf(core): [Init Reflection 1] Probe class availability without initializing (#5635) * perf(core): Probe class availability without initializing the class LoadClass.loadClass used Class.forName(name) which initializes the class. Used purely for availability probing during init, this eagerly runs unrelated static initializers (e.g. Compose's Owner, the fragment integration). Use Class.forName(name, false, classLoader) so the class is only initialized lazily on first real use. Co-Authored-By: Claude Opus 4.8 (1M context) * changelog * changelog: move init reflection entries to Performance * perf(core): Limit no-init class probing to isClassAvailable The previous change made loadClass itself skip class initialization, which affected callers that load a class to actually use it (NDK integration, OTEL span factory and scopes storage). Restore loadClass to its initializing behavior and confine the non-initializing probe to isClassAvailable, which is only ever used for classpath availability checks. This keeps SDK init cheap while leaving real-use callers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + .../main/java/io/sentry/util/LoadClass.java | 34 ++++++++- .../test/java/io/sentry/util/LoadClassTest.kt | 70 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/util/LoadClassTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a37500b9a1..dce1fd22d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Performance - Speed up touch gesture target detection on deeply nested view hierarchies by hit-testing in local coordinates instead of calling `getLocationOnScreen` per view ([#5595](https://github.com/getsentry/sentry-java/pull/5595)) +- Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635)) ## 8.46.0 diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index 1946ce8381f..2c39cace39b 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -12,15 +12,23 @@ public class LoadClass { /** - * Try to load a class via reflection + * Loads and initializes a class via reflection. Use this when you intend to actually use the + * class (e.g. instantiate it or invoke its methods). The returned class is fully initialized, so + * its static initializers run. To merely check whether a class is on the classpath, use {@link + * #isClassAvailable} instead, which avoids running those initializers. * * @param clazz the full class name * @param logger an instance of ILogger * @return a Class<?> if it's available, or null */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { + return loadClass(clazz, logger, true); + } + + private @Nullable Class loadClass( + final @NotNull String clazz, final @Nullable ILogger logger, final boolean initialize) { try { - return Class.forName(clazz); + return Class.forName(clazz, initialize, LoadClass.class.getClassLoader()); } catch (ClassNotFoundException e) { if (logger != null) { logger.log(SentryLevel.INFO, "Class not available: " + clazz); @@ -37,8 +45,19 @@ public class LoadClass { return null; } + /** + * Probes whether a class is on the classpath without initializing it. Use this for availability + * checks (e.g. deciding whether to register an integration); the class is not initialized, so its + * static initializers do not run until something actually uses it. This keeps SDK init cheap by + * not triggering unrelated initializers. If you need to use the class, use {@link #loadClass} + * instead. + * + * @param clazz the full class name + * @param logger an instance of ILogger + * @return true if the class is on the classpath + */ public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) { - return loadClass(clazz, logger) != null; + return loadClass(clazz, logger, false) != null; } public boolean isClassAvailable( @@ -46,6 +65,15 @@ public boolean isClassAvailable( return isClassAvailable(clazz, options != null ? options.getLogger() : null); } + /** + * Like {@link #isClassAvailable}, but defers the (non-initializing) availability check until the + * result is first read. Use this when the check itself should not run during SDK init but only + * later, on first access. + * + * @param clazz the full class name + * @param logger an instance of ILogger + * @return a lazily-evaluated availability check + */ public LazyEvaluator isClassAvailableLazy( final @NotNull String clazz, final @Nullable ILogger logger) { return new LazyEvaluator<>(() -> isClassAvailable(clazz, logger)); diff --git a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt new file mode 100644 index 00000000000..7a8bc802049 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt @@ -0,0 +1,70 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class LoadClassTest { + @Test + fun `loadClass returns the class when it is available`() { + assertNotNull(LoadClass().loadClass("io.sentry.SentryEvent", null)) + } + + @Test + fun `loadClass returns null when the class is not available`() { + assertNull(LoadClass().loadClass("io.sentry.ThisClassDoesNotExist", null)) + } + + @Test + fun `isClassAvailable reflects whether the class is on the classpath`() { + val loadClass = LoadClass() + assertNotNull(loadClass.loadClass("io.sentry.SentryEvent", null)) + assertFalse( + loadClass.isClassAvailable("io.sentry.ThisClassDoesNotExist", null as io.sentry.ILogger?) + ) + } + + @Test + fun `isClassAvailable does not run the static initializer of the probed class`() { + // Reading the flag initializes the flag holder, not the probe. + assertFalse(IsClassAvailableNoInitFlag.initialized) + + // Obtaining the name via ::class.java does not initialize the probe either. + LoadClass() + .isClassAvailable(IsClassAvailableNoInitProbe::class.java.name, null as io.sentry.ILogger?) + + // Availability probing must not trigger the probe's static initializer. + assertFalse(IsClassAvailableNoInitFlag.initialized) + } + + @Test + fun `loadClass runs the static initializer of the loaded class`() { + assertFalse(LoadClassInitFlag.initialized) + + LoadClass().loadClass(LoadClassInitProbe::class.java.name, null) + + assertTrue(LoadClassInitFlag.initialized) + } +} + +private object IsClassAvailableNoInitFlag { + @JvmField var initialized = false +} + +private object IsClassAvailableNoInitProbe { + init { + IsClassAvailableNoInitFlag.initialized = true + } +} + +private object LoadClassInitFlag { + @JvmField var initialized = false +} + +private object LoadClassInitProbe { + init { + LoadClassInitFlag.initialized = true + } +}