Skip to content

perf(core): [Init Reflection 1] Probe class availability without initializing#5635

Open
runningcode wants to merge 3 commits into
no/perf-init-reflectionfrom
no/perf-init-reflection-no-init
Open

perf(core): [Init Reflection 1] Probe class availability without initializing#5635
runningcode wants to merge 3 commits into
no/perf-init-reflectionfrom
no/perf-init-reflection-no-init

Conversation

@runningcode

@runningcode runningcode commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

PR Stack (Init Reflection)


Part of JAVA-587

📜 Description

LoadClass.loadClass used Class.forName(name), which initializes the class. Since this method is used purely to probe whether an optional integration is on the classpath during SentryAndroid.init, that eagerly runs unrelated static initializers — the customer trace shows androidx.compose.ui.node.Owner.<clinit> and FragmentLifecycleIntegration.<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 LoadClassTest including a guard asserting that probing a class does not run its static initializer; existing init/integration tests pass unchanged.

📝 Checklist

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • No breaking change or entry added to the changelog.

🔮 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:

old Class.forName(name) new Class.forName(name, false, loader)
target-class <clinit> invocations under the probe 1 0

The 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> and FragmentLifecycleIntegration.<clinit> running during init. (Method tracing inflates the absolute <clinit> time, so only the invocation count is reported.)

⚠️ Merge this PR using a merge commit (not squash). Only the collection branch is squash-merged into main.

runningcode and others added 2 commits June 25, 2026 13:31
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>
@sentry

sentry Bot commented Jun 25, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.45.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 316.64 ms 393.06 ms 76.42 ms
Size 0 B 0 B 0 B

@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

JAVA-587

@runningcode runningcode marked this pull request as ready for review June 25, 2026 12:03
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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants