perf(core): [Init Reflection 1] Probe class availability without initializing#5635
perf(core): [Init Reflection 1] Probe class availability without initializing#5635runningcode wants to merge 3 commits into
Conversation
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) <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| return Class.forName(clazz); | ||
| // Don't initialize the class just to probe for availability; it gets initialized lazily on | ||
| // first use. This avoids running unrelated static initializers during SDK init. | ||
| return Class.forName(clazz, false, LoadClass.class.getClassLoader()); |
There was a problem hiding this comment.
In SentryNdk the static init block will now run much later, potentially causing ANRs.
Previously Class.forName triggered static block early and gave it time to run SentryNdk.loadNativeLibraries() in the background before we waited for loadLibraryLatch.await in the init method.
Now this may cause applications to spend more time waiting on main thread. Worst case would be 2s additional wait on main thread.
There was a problem hiding this comment.
This also leads to a very theoretical case of classes being handed back that then fail when creating an instance. This could bite us on the OTel span factory and scopes storage. One fix here could be to just catch Throwable in SpanFactoryFactory and ScopesStorageFactory instead of all the individual exceptions where we might then miss ExceptionInInitializerError/NoClassDefFoundError/LinkageError.
But looking at the finding above, it may make sense to allow controlling true/false for the Class.forName call from the caller.
This however would increase complexity of caching results since we might have invoked Class.forName with false already and cached the result, then another caller might want true but since we already cached it it won't do it.
PR Stack (Init Reflection)
Part of JAVA-587
📜 Description
LoadClass.loadClassusedClass.forName(name), which initializes the class. Since this method is used purely to probe whether an optional integration is on the classpath duringSentryAndroid.init, that eagerly runs unrelated static initializers — the customer trace showsandroidx.compose.ui.node.Owner.<clinit>andFragmentLifecycleIntegration.<clinit>executing under the availability check.This switches to
Class.forName(name, false, classLoader)so the class is loaded but only initialized lazily on first real use (e.g. when it's actually instantiated).💡 Motivation and Context
First of three stacked PRs reducing reflection cost on the init path, from the customer-provided Perfetto trace in the Reduce SDK init time [Android] project (JAVA-586 area).
💚 How did you test it?
New
LoadClassTestincluding a guard asserting that probing a class does not run its static initializer; existing init/integration tests pass unchanged.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
PR 2 caches lookups and collapses double-probes; PR 3 gates the Compose probes behind their features.
⏱️ Pixel 3 benchmark (ART method trace → Perfetto trace_processor)
Probing a class that has a (deliberately heavy) static initializer:
Class.forName(name)Class.forName(name, false, loader)<clinit>invocations under the probeThe static initializer runs under the old probe and is entirely skipped under the new one. In the production trace this was
androidx.compose.ui.node.Owner.<clinit>andFragmentLifecycleIntegration.<clinit>running during init. (Method tracing inflates the absolute<clinit>time, so only the invocation count is reported.)