Skip to content

perf(core): Lazily allocate AutoClosableReentrantLock (JAVA-588)#5643

Draft
runningcode wants to merge 2 commits into
mainfrom
no/java-588-lazy-lock-allocation
Draft

perf(core): Lazily allocate AutoClosableReentrantLock (JAVA-588)#5643
runningcode wants to merge 2 commits into
mainfrom
no/java-588-lazy-lock-allocation

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

io.sentry.util.AutoClosableReentrantLock extended java.util.concurrent.locks.ReentrantLock, and ~57 SDK classes create one eagerly in a field initializer. Constructing the SDK object graph therefore allocated a ReentrantLock (plus its AbstractQueuedSynchronizer Sync) per object — even for objects whose lock is never acquired.

This change holds the ReentrantLock internally and creates it lazily on the first acquire(), using an AtomicReferenceFieldUpdater CAS so the lazy creation is atomic and stays Loom-friendly (no synchronized, preserving the intent of #3715). The lifecycle token captures the resolved lock instance so close() unlocks the correct one.

💡 Motivation and Context

Part of the Reduce SDK init time [Android] effort (JAVA-588). A customer-provided Perfetto trace showed ~81 ReentrantLock allocations on the main thread under SentryAndroid.init, contributing GC pressure and main-thread CPU. With lazy allocation, only locks actually acquired during init allocate; the many never-contended locks no longer allocate at construction.

Compatibility note: this is technically a binary-compatibility change — AutoClosableReentrantLock no longer extends ReentrantLock, so the inherited ReentrantLock public methods leave its API surface (reflected in sentry.api). A usage audit confirmed every call site uses only acquire() (try-with-resources); nothing calls lock()/unlock()/tryLock()/isHeldByCurrentThread() directly or types a field/param as ReentrantLock, so no caller is affected.

A new on-device A/B benchmark (LockAllocationBenchmarkTest, not run in CI) quantifies the allocation reduction and confirms acquire() hot-path throughput is unchanged.

💚 How did you test it?

Unit tests in AutoClosableReentrantLockTest (lazy creation, reentrancy, and an 8-thread × 1000-iteration mutual-exclusion stress test); full :sentry:test suite green.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Biggest remaining main-thread init cost in the same trace is Manifest parsing (~147ms) — tracked separately (SDK-1322 / JAVA-531, compile-time injection).

Fixes JAVA-588

AutoClosableReentrantLock extended ReentrantLock, so every SDK object
holding one allocated a ReentrantLock (and its AbstractQueuedSynchronizer)
eagerly in its field initializer. A customer Perfetto trace showed ~81 such
allocations on the main thread during SentryAndroid.init, many for locks
that are never acquired during init.

Hold the ReentrantLock internally and create it lazily on first acquire(),
using an AtomicReferenceFieldUpdater CAS so creation stays atomic and
Loom-friendly (no synchronized, preserving #3715). Every call site uses
acquire() only, so dropping the ReentrantLock superclass touches no caller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

JAVA-588

@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Please consider adding a changelog entry for the next release.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Performance

- Lazily allocate AutoClosableReentrantLock (JAVA-588) ([#5643](https://github.com/getsentry/sentry-java/pull/5643))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against 510e290

@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 330.67 ms 380.76 ms 50.09 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
18c0bc2 306.73 ms 349.77 ms 43.03 ms
0eaac1e 316.82 ms 357.34 ms 40.52 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
fc5ccaf 276.52 ms 370.46 ms 93.93 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms
37ec571 366.04 ms 424.28 ms 58.23 ms
9fbb112 361.43 ms 427.57 ms 66.14 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
ff8eea4 313.42 ms 337.08 ms 23.66 ms

App size

Revision Plain With Sentry Diff
18c0bc2 1.58 MiB 2.13 MiB 557.33 KiB
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
e2dce0b 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B
37ec571 0 B 0 B 0 B
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
ff8eea4 1.58 MiB 2.28 MiB 718.64 KiB

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.

1 participant