Commit 7414e9b
fix(replay): Prevent concurrent PixelCopy frame access (#5808)
* fix(replay): Prevent concurrent PixelCopy frame access
Keep PixelCopy, masking, compositing, and cleanup from accessing the shared bitmap concurrently.
Fixes GH-5340
Co-Authored-By: Codex <noreply@openai.com>
* changelog
* fix(android-replay): Preserve pending PixelCopy capture
Re-arm the recorder's content-change gate when a capture is skipped because another frame is still in flight. This ensures the latest UI state is retried on the next capture interval.
Refs GH-5340
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* fix(replay): Release PixelCopy frame gate and cleanup on failure paths
Two bugbot findings on #5808:
- Frame gate stuck on callback errors: viewhierarchy traversal or
captureSurfaceViews throwing between the PixelCopy success check and
the executor submit left frameInFlight = true forever, silently
wedging all future captures. Wrap the post-success block in a
try/finally that releases the gate unless work was successfully
handed off.
- Cleanup lost after executor shutdown: close() typically runs after
ReplayIntegration has shut down the replay executor, so submit()
returns null and the bitmap + maskRenderer were never released. Fall
back to running cleanup inline in that case.
* chore(replay): Drop redundant handedOff flag in capture callback
The mask branch already knows via 'submitted == null' whether the executor
took ownership; the surface-view branch always hands off. Flatten to
explicit finishFrame() calls on the two failure paths (mask null-submit,
outer catch) instead of tracking handoff state across a try/finally.
* fix(replay): Distinguish inline execution from executor rejection
ReplayExecutorService.submit previously returned null both when the caller
was on the worker thread (task ran inline) and when the executor rejected
the submission (task did NOT run). Callers had no way to tell them apart.
Return a CompletedFuture sentinel for inline execution; null now means
only rejection. Also narrow PixelCopyStrategy's frame-processing catch
from Throwable to RuntimeException so OOM/LinkageError still propagate.
* chore(replay): Drop redundant cleanupScheduled guard
Cleanup body is already idempotent (screenshot.isRecycled check,
MaskRenderer.close guards on isInitialized + isRecycled). A stray extra
scheduleCleanup would just submit a no-op — not worth the AtomicBoolean.
* test(replay): Fix recursive close and assert masking is skipped on close race
The mock executor closed the strategy on every submit(); since close()
itself submits the cleanup task, this recursed
close() -> scheduleCleanup() -> submit() until the stack overflowed.
Close only when the mask task is submitted.
The prior "does not crash" assertion was also vacuous under the
frameInFlight gate (passed even with the isClosed guard removed). Assert
instead that no screenshot is emitted once close() races masking, which
fails if the guard in applyMaskingAndNotify is removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(replay): Guard finishFrame cleanup with CAS to avoid recycling a bitmap a new capture is using
finishFrame cleared frameInFlight before checking isClosed, so a new capture could
take the gate and start PixelCopy into the shared screenshot while the old
finishFrame went on to scheduleCleanup after close() flipped isClosed — recycling
the bitmap mid-write. Re-take the gate with compareAndSet before cleaning up so the
losing frame backs off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(replay): Make close's idle cleanup claim the gate atomically
close() checked !frameInFlight.get() non-atomically before scheduleCleanup, so a
capture racing in after the check could take the gate, see isClosed, and have its
finishFrame schedule a second cleanup. Extract the shared "claim the gate, then the
winner cleans up once" invariant into cleanUpIfIdle() so close() and finishFrame()
use the same CAS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(replay): Return CompletedFuture from inlineExecutor to match ReplayExecutorService
inlineExecutor() returned null from submit, which under the executor's contract means
"rejected" — making capture()'s null-fallback finishFrame run on top of the mask
task's own finally, a double-release production never hits on the inline path. Return
CompletedFuture so the fixture matches the real inline semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(replay): Hold frame gate across emitLastScreenshot to prevent concurrent bitmap access
emitLastScreenshot runs on the main thread, so the downstream consumer queues
its bitmap read (JPEG compress) to the executor asynchronously. Without the
gate, the next capture tick's PixelCopy.request writes into the shared
screenshot while the queued read is still in flight. Submit the consumer call
to the executor so it runs inline on the worker thread while the gate is held,
same pattern as the masked capture path.
Also fixes the close-race test mock to return CompletedFuture instead of null,
matching the executor contract (same fix as inlineExecutor in cd3a7c0).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 6335e35 commit 7414e9b
4 files changed
Lines changed: 434 additions & 68 deletions
File tree
- sentry-android-replay/src
- main/java/io/sentry/android/replay
- screenshot
- util
- test/java/io/sentry/android/replay/screenshot
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
Lines changed: 147 additions & 61 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| |||
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
80 | 87 | | |
81 | 88 | | |
| 89 | + | |
82 | 90 | | |
83 | 91 | | |
84 | 92 | | |
| |||
90 | 98 | | |
91 | 99 | | |
92 | 100 | | |
| 101 | + | |
93 | 102 | | |
94 | 103 | | |
95 | 104 | | |
96 | 105 | | |
97 | 106 | | |
98 | 107 | | |
99 | 108 | | |
| 109 | + | |
100 | 110 | | |
101 | 111 | | |
102 | 112 | | |
103 | 113 | | |
104 | 114 | | |
| 115 | + | |
105 | 116 | | |
106 | 117 | | |
107 | 118 | | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
125 | 146 | | |
| 147 | + | |
| 148 | + | |
126 | 149 | | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
138 | 167 | | |
139 | 168 | | |
140 | 169 | | |
| |||
143 | 172 | | |
144 | 173 | | |
145 | 174 | | |
| 175 | + | |
146 | 176 | | |
147 | 177 | | |
148 | 178 | | |
| |||
272 | 302 | | |
273 | 303 | | |
274 | 304 | | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
282 | 314 | | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
302 | 334 | | |
303 | | - | |
304 | | - | |
305 | | - | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
306 | 345 | | |
307 | 346 | | |
308 | 347 | | |
| |||
322 | 361 | | |
323 | 362 | | |
324 | 363 | | |
325 | | - | |
326 | | - | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
327 | 385 | | |
328 | 386 | | |
329 | 387 | | |
330 | 388 | | |
331 | 389 | | |
332 | 390 | | |
333 | | - | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
334 | 415 | | |
335 | 416 | | |
336 | 417 | | |
| |||
344 | 425 | | |
345 | 426 | | |
346 | 427 | | |
347 | | - | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
348 | 434 | | |
349 | 435 | | |
350 | 436 | | |
| |||
Lines changed: 25 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
17 | 28 | | |
18 | 29 | | |
19 | | - | |
20 | 30 | | |
21 | | - | |
| 31 | + | |
22 | 32 | | |
23 | 33 | | |
24 | 34 | | |
| |||
68 | 78 | | |
69 | 79 | | |
70 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
0 commit comments