Skip to content

Commit 3471b23

Browse files
runningcodeclaude
andcommitted
perf(core): Cache class lookups and collapse double probes
Class availability is fixed for the lifetime of the process, so cache LoadClass results to avoid repeated Class.forName lookups (and the exceptions thrown for absent classes) when the same class is probed more than once. Also collapse the isClassAvailable-then-loadClass double probe in the OpenTelemetry span/scopes factories into a single loadClass call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7a33c9c commit 3471b23

3 files changed

Lines changed: 52 additions & 35 deletions

File tree

sentry/src/main/java/io/sentry/ScopesStorageFactory.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,23 @@ public final class ScopesStorageFactory {
2323
private static @NotNull IScopesStorage createInternal(
2424
final @NotNull LoadClass loadClass, final @NotNull ILogger logger) {
2525
if (Platform.isJvm()) {
26-
if (loadClass.isClassAvailable(OTEL_SCOPES_STORAGE, logger)) {
27-
Class<?> otelScopesStorageClazz = loadClass.loadClass(OTEL_SCOPES_STORAGE, logger);
28-
if (otelScopesStorageClazz != null) {
29-
try {
30-
final @Nullable Object otelScopesStorage =
31-
otelScopesStorageClazz.getDeclaredConstructor().newInstance();
32-
if (otelScopesStorage instanceof IScopesStorage) {
33-
return (IScopesStorage) otelScopesStorage;
34-
}
35-
} catch (InstantiationException e) {
36-
// TODO log
37-
} catch (IllegalAccessException e) {
38-
// TODO log
39-
} catch (InvocationTargetException e) {
40-
// TODO log
41-
} catch (NoSuchMethodException e) {
42-
// TODO log
26+
final @Nullable Class<?> otelScopesStorageClazz =
27+
loadClass.loadClass(OTEL_SCOPES_STORAGE, logger);
28+
if (otelScopesStorageClazz != null) {
29+
try {
30+
final @Nullable Object otelScopesStorage =
31+
otelScopesStorageClazz.getDeclaredConstructor().newInstance();
32+
if (otelScopesStorage instanceof IScopesStorage) {
33+
return (IScopesStorage) otelScopesStorage;
4334
}
35+
} catch (InstantiationException e) {
36+
// TODO log
37+
} catch (IllegalAccessException e) {
38+
// TODO log
39+
} catch (InvocationTargetException e) {
40+
// TODO log
41+
} catch (NoSuchMethodException e) {
42+
// TODO log
4443
}
4544
}
4645
}

sentry/src/main/java/io/sentry/SpanFactoryFactory.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,23 @@ public final class SpanFactoryFactory {
1515
public static @NotNull ISpanFactory create(
1616
final @NotNull LoadClass loadClass, final @NotNull ILogger logger) {
1717
if (Platform.isJvm()) {
18-
if (loadClass.isClassAvailable(OTEL_SPAN_FACTORY, logger)) {
19-
Class<?> otelSpanFactoryClazz = loadClass.loadClass(OTEL_SPAN_FACTORY, logger);
20-
if (otelSpanFactoryClazz != null) {
21-
try {
22-
final @Nullable Object otelSpanFactory =
23-
otelSpanFactoryClazz.getDeclaredConstructor().newInstance();
24-
if (otelSpanFactory instanceof ISpanFactory) {
25-
return (ISpanFactory) otelSpanFactory;
26-
}
27-
} catch (InstantiationException e) {
28-
// TODO log
29-
} catch (IllegalAccessException e) {
30-
// TODO log
31-
} catch (InvocationTargetException e) {
32-
// TODO log
33-
} catch (NoSuchMethodException e) {
34-
// TODO log
18+
final @Nullable Class<?> otelSpanFactoryClazz =
19+
loadClass.loadClass(OTEL_SPAN_FACTORY, logger);
20+
if (otelSpanFactoryClazz != null) {
21+
try {
22+
final @Nullable Object otelSpanFactory =
23+
otelSpanFactoryClazz.getDeclaredConstructor().newInstance();
24+
if (otelSpanFactory instanceof ISpanFactory) {
25+
return (ISpanFactory) otelSpanFactory;
3526
}
27+
} catch (InstantiationException e) {
28+
// TODO log
29+
} catch (IllegalAccessException e) {
30+
// TODO log
31+
} catch (InvocationTargetException e) {
32+
// TODO log
33+
} catch (NoSuchMethodException e) {
34+
// TODO log
3635
}
3736
}
3837
}

sentry/src/main/java/io/sentry/util/LoadClass.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
import io.sentry.ILogger;
55
import io.sentry.SentryLevel;
66
import io.sentry.SentryOptions;
7+
import java.util.Map;
8+
import java.util.concurrent.ConcurrentHashMap;
79
import org.jetbrains.annotations.NotNull;
810
import org.jetbrains.annotations.Nullable;
911

1012
/** An Adapter for making Class.forName testable */
1113
@Open
1214
public class LoadClass {
1315

16+
/** Sentinel cached for class names that are known to be unavailable. */
17+
private static final Object NOT_AVAILABLE = new Object();
18+
19+
/**
20+
* Whether a class is on the classpath does not change during the lifetime of the process, so
21+
* results are cached to avoid repeated {@link Class#forName} lookups (and the exceptions they
22+
* throw for absent classes) when the same class is probed more than once.
23+
*/
24+
private static final Map<String, Object> CLASSES = new ConcurrentHashMap<>();
25+
1426
/**
1527
* Try to load a class via reflection
1628
*
@@ -19,11 +31,18 @@ public class LoadClass {
1931
* @return a Class&lt;?&gt; if it's available, or null
2032
*/
2133
public @Nullable Class<?> loadClass(final @NotNull String clazz, final @Nullable ILogger logger) {
34+
final @Nullable Object cached = CLASSES.get(clazz);
35+
if (cached != null) {
36+
return cached == NOT_AVAILABLE ? null : (Class<?>) cached;
37+
}
2238
try {
2339
// Don't initialize the class just to probe for availability; it gets initialized lazily on
2440
// first use. This avoids running unrelated static initializers during SDK init.
25-
return Class.forName(clazz, false, LoadClass.class.getClassLoader());
41+
final Class<?> loadedClass = Class.forName(clazz, false, LoadClass.class.getClassLoader());
42+
CLASSES.put(clazz, loadedClass);
43+
return loadedClass;
2644
} catch (ClassNotFoundException e) {
45+
CLASSES.put(clazz, NOT_AVAILABLE);
2746
if (logger != null) {
2847
logger.log(SentryLevel.INFO, "Class not available: " + clazz);
2948
}

0 commit comments

Comments
 (0)