AppStartExtension holds its own lock while calling finish() on the extended span (finishExtendedAppStart) and on the standalone app.start transaction (finishTransaction).
Finishing captures the transaction synchronously — SentryTracer.finish() calls scopes.captureTransaction(...) on the calling thread, which runs PerformanceAndroidEventProcessor.process(). That processor takes its own lock and then calls back into AppStartExtension. The two locks are therefore acquired in opposite orders:
| Side |
Where, at cc59f486da |
| extension lock → processor lock |
AppStartExtension.finishExtendedAppStart():111 and finishTransaction():148 hold the lock across span.finish() / transaction.finish(), which capture re-entrantly |
| processor lock → extension lock |
PerformanceAndroidEventProcessor.process() acquires its lock at :82, then calls extension.isExtended() at :112 and getExtendedEndTime() at :113 inside it |
Impact
sdk-crashes-java, last 90 days — ANR events with AppStartExtension on the stack:
| Release |
ANR events |
Customer projects |
| 8.48.0 |
13 |
4 |
| 8.49.0 |
751 |
4 |
| 8.50.0 / 8.50.1 |
4 |
3 |
| 8.51.0 |
16 |
6 |
Blocked waiting for the lock — SDK-CRASHES-JAVA-5MBP, main thread, foreground app start, SDK 8.49.0:
ActivityLifecycleIntegration.onFirstFrameDrawn(:812)
ActivityLifecycleIntegration.finishAppStartSpan(:992)
AppStartExtension.finishTransaction(AppStartExtension.java:148)
AutoClosableReentrantLock.acquire(:37)
ReentrantLock.lock(:323) <- parked
In the 8.49.0 tag, AppStartExtension.java:148 is the lock.acquire() at the top of finishTransaction.
Holding the lock — SDK-CRASHES-JAVA-5MEQ, 87 occurrences:
AppStartMetrics.lambda$scheduleHeadlessAppStartCheckOnMain$0
AppStartExtension.finishTransaction
SentryTracer.finish -> SentryTracer.finish
These events contain only the main thread, so the full cycle is not directly visible. What is confirmed is the mechanism: the extension lock held across transaction.finish(), with real users' main threads blocking on it until the OS files an ANR. Whether a given event is a permanent deadlock or contention that merely exceeded the ANR threshold cannot be distinguished from these traces — both are addressed by the same fix.
Introduced by
PR #5604 (feat(extend-app-start): App start extension API), commit cc59f486da, merged 2026-07-07. Both halves of the cycle landed in that one commit. First released in 8.48.0, which is exactly where the ANR series begins.
Fix
PR #6007 reads the fields under the lock and performs the finish outside it, so the extension never holds its lock across a call that re-enters the SDK. Span and transaction finishing are idempotent (Span.finish guards with a CAS), so racing callers are safe.
Follow-up worth considering: extendAppStart() still calls the listener (and a user-supplied logger) under the lock. That path does not capture, so it is not part of this cycle.
AppStartExtensionholds its own lock while callingfinish()on the extended span (finishExtendedAppStart) and on the standaloneapp.starttransaction (finishTransaction).Finishing captures the transaction synchronously —
SentryTracer.finish()callsscopes.captureTransaction(...)on the calling thread, which runsPerformanceAndroidEventProcessor.process(). That processor takes its own lock and then calls back intoAppStartExtension. The two locks are therefore acquired in opposite orders:cc59f486daAppStartExtension.finishExtendedAppStart():111andfinishTransaction():148hold the lock acrossspan.finish()/transaction.finish(), which capture re-entrantlyPerformanceAndroidEventProcessor.process()acquires its lock at :82, then callsextension.isExtended()at :112 andgetExtendedEndTime()at :113 inside itImpact
sdk-crashes-java, last 90 days — ANR events withAppStartExtensionon the stack:Blocked waiting for the lock — SDK-CRASHES-JAVA-5MBP, main thread, foreground app start, SDK 8.49.0:
In the
8.49.0tag,AppStartExtension.java:148is thelock.acquire()at the top offinishTransaction.Holding the lock — SDK-CRASHES-JAVA-5MEQ, 87 occurrences:
These events contain only the main thread, so the full cycle is not directly visible. What is confirmed is the mechanism: the extension lock held across
transaction.finish(), with real users' main threads blocking on it until the OS files an ANR. Whether a given event is a permanent deadlock or contention that merely exceeded the ANR threshold cannot be distinguished from these traces — both are addressed by the same fix.Introduced by
PR #5604 (
feat(extend-app-start): App start extension API), commitcc59f486da, merged 2026-07-07. Both halves of the cycle landed in that one commit. First released in 8.48.0, which is exactly where the ANR series begins.Fix
PR #6007 reads the fields under the lock and performs the finish outside it, so the extension never holds its lock across a call that re-enters the SDK. Span and transaction finishing are idempotent (
Span.finishguards with a CAS), so racing callers are safe.Follow-up worth considering:
extendAppStart()still calls the listener (and a user-supplied logger) under the lock. That path does not capture, so it is not part of this cycle.