-
-
Notifications
You must be signed in to change notification settings - Fork 475
perf(core): [Init Reflection 2] Cache class lookups and collapse double probes #5636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
62133fc
7a33c9c
552d466
f6074f1
5b872a7
e6d30e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,24 +4,52 @@ | |
| import io.sentry.ILogger; | ||
| import io.sentry.SentryLevel; | ||
| import io.sentry.SentryOptions; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** An Adapter for making Class.forName testable */ | ||
| @Open | ||
| public class LoadClass { | ||
|
|
||
| /** Sentinel cached for class names that are known to be unavailable. */ | ||
| private static final Object NOT_AVAILABLE = new Object(); | ||
|
|
||
| /** | ||
| * Try to load a class via reflection | ||
| * Whether a class is on the classpath does not change during the lifetime of the process, so | ||
| * results are cached to avoid repeated {@link Class#forName} lookups (and the exceptions they | ||
| * throw for absent classes) when the same class is probed more than once. | ||
| */ | ||
| private static final Map<String, Object> CLASSES = new ConcurrentHashMap<>(); | ||
|
|
||
| /** | ||
| * 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) { | ||
| final @Nullable Object cached = CLASSES.get(clazz); | ||
| if (cached != null) { | ||
| return cached == NOT_AVAILABLE ? null : (Class<?>) cached; | ||
| } | ||
| try { | ||
| return Class.forName(clazz); | ||
| final Class<?> loadedClass = | ||
| Class.forName(clazz, initialize, LoadClass.class.getClassLoader()); | ||
| CLASSES.put(clazz, loadedClass); | ||
| return loadedClass; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cache ignores initialize flagMedium Severity The static cache keys lookups only by class name, but Reviewed by Cursor Bugbot for commit e6d30e8. Configure here. |
||
| } catch (ClassNotFoundException e) { | ||
| CLASSES.put(clazz, NOT_AVAILABLE); | ||
| if (logger != null) { | ||
| logger.log(SentryLevel.INFO, "Class not available: " + clazz); | ||
| } | ||
|
|
@@ -37,15 +65,35 @@ 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( | ||
| final @NotNull String clazz, final @Nullable SentryOptions options) { | ||
| 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<Boolean> isClassAvailableLazy( | ||
| final @NotNull String clazz, final @Nullable ILogger logger) { | ||
| return new LazyEvaluator<>(() -> isClassAvailable(clazz, logger)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The cache in
LoadClassignores theinitializeparameter, causing it to potentially return an uninitialized class when an initialized one is expected.Severity: MEDIUM
Suggested Fix
The cache key should incorporate the
initializeparameter to differentiate between initialized and uninitialized class states. Alternatively, only cache fully initialized classes, or re-evaluate and load the class if a request withinitialize=truefinds an uninitialized class in the cache.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.