Skip to content

Commit 91bb628

Browse files
authored
[SR] Fix ANRs and speed up (#4001)
* Get rid of the lock on touch events * pre-allocate some things for gesture converter * have one less thread switch for re]play * update * Changelog
1 parent 90eb1e3 commit 91bb628

File tree

13 files changed

+69
-70
lines changed

13 files changed

+69
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Change TTFD timeout to 25 seconds ([#3984](https://github.com/getsentry/sentry-java/pull/3984))
88
- Session Replay: Fix memory leak when masking Compose screens ([#3985](https://github.com/getsentry/sentry-java/pull/3985))
9+
- Session Replay: Fix potential ANRs in `GestureRecorder` ([#4001](https://github.com/getsentry/sentry-java/pull/4001))
910

1011
## 7.19.0
1112

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import io.sentry.android.replay.gestures.GestureRecorder
2929
import io.sentry.android.replay.gestures.TouchRecorderCallback
3030
import io.sentry.android.replay.util.MainLooperHandler
3131
import io.sentry.android.replay.util.appContext
32+
import io.sentry.android.replay.util.gracefullyShutdown
3233
import io.sentry.android.replay.util.sample
3334
import io.sentry.android.replay.util.submitSafely
3435
import io.sentry.cache.PersistingScopeObserver
@@ -46,8 +47,9 @@ import io.sentry.util.Random
4647
import java.io.Closeable
4748
import java.io.File
4849
import java.util.LinkedList
50+
import java.util.concurrent.Executors
51+
import java.util.concurrent.ThreadFactory
4952
import java.util.concurrent.atomic.AtomicBoolean
50-
import kotlin.LazyThreadSafetyMode.NONE
5153

5254
public class ReplayIntegration(
5355
private val context: Context,
@@ -93,7 +95,10 @@ public class ReplayIntegration(
9395
private var recorder: Recorder? = null
9496
private var gestureRecorder: GestureRecorder? = null
9597
private val random by lazy { Random() }
96-
internal val rootViewsSpy by lazy(NONE) { RootViewsSpy.install() }
98+
internal val rootViewsSpy by lazy { RootViewsSpy.install() }
99+
private val replayExecutor by lazy {
100+
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
101+
}
97102

98103
// TODO: probably not everything has to be thread-safe here
99104
internal val isEnabled = AtomicBoolean(false)
@@ -123,7 +128,7 @@ public class ReplayIntegration(
123128
}
124129

125130
this.hub = hub
126-
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler)
131+
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor)
127132
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
128133
isEnabled.set(true)
129134

@@ -166,9 +171,9 @@ public class ReplayIntegration(
166171

167172
recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
168173
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
169-
SessionCaptureStrategy(options, hub, dateProvider, replayCacheProvider = replayCacheProvider)
174+
SessionCaptureStrategy(options, hub, dateProvider, replayExecutor, replayCacheProvider)
170175
} else {
171-
BufferCaptureStrategy(options, hub, dateProvider, random, replayCacheProvider = replayCacheProvider)
176+
BufferCaptureStrategy(options, hub, dateProvider, random, replayExecutor, replayCacheProvider)
172177
}
173178

174179
captureStrategy?.start(recorderConfig)
@@ -229,7 +234,6 @@ public class ReplayIntegration(
229234
gestureRecorder?.stop()
230235
captureStrategy?.stop()
231236
isRecording.set(false)
232-
captureStrategy?.close()
233237
captureStrategy = null
234238
}
235239

@@ -264,6 +268,7 @@ public class ReplayIntegration(
264268
recorder?.close()
265269
recorder = null
266270
rootViewsSpy.close()
271+
replayExecutor.gracefullyShutdown(options)
267272
}
268273

269274
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -405,4 +410,13 @@ public class ReplayIntegration(
405410
private class PreviousReplayHint : Backfillable {
406411
override fun shouldEnrich(): Boolean = false
407412
}
413+
414+
private class ReplayExecutorServiceThreadFactory : ThreadFactory {
415+
private var cnt = 0
416+
override fun newThread(r: Runnable): Thread {
417+
val ret = Thread(r, "SentryReplayIntegration-" + cnt++)
418+
ret.setDaemon(true)
419+
return ret
420+
}
421+
}
408422
}

sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import io.sentry.SentryReplayOptions
2525
import io.sentry.android.replay.util.MainLooperHandler
2626
import io.sentry.android.replay.util.addOnDrawListenerSafe
2727
import io.sentry.android.replay.util.getVisibleRects
28-
import io.sentry.android.replay.util.gracefullyShutdown
2928
import io.sentry.android.replay.util.removeOnDrawListenerSafe
3029
import io.sentry.android.replay.util.submitSafely
3130
import io.sentry.android.replay.util.traverse
@@ -34,7 +33,7 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarc
3433
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
3534
import java.io.File
3635
import java.lang.ref.WeakReference
37-
import java.util.concurrent.Executors
36+
import java.util.concurrent.ScheduledExecutorService
3837
import java.util.concurrent.ThreadFactory
3938
import java.util.concurrent.atomic.AtomicBoolean
4039
import kotlin.LazyThreadSafetyMode.NONE
@@ -44,13 +43,11 @@ import kotlin.math.roundToInt
4443
internal class ScreenshotRecorder(
4544
val config: ScreenshotRecorderConfig,
4645
val options: SentryOptions,
47-
val mainLooperHandler: MainLooperHandler,
46+
private val mainLooperHandler: MainLooperHandler,
47+
private val recorder: ScheduledExecutorService,
4848
private val screenshotRecorderCallback: ScreenshotRecorderCallback?
4949
) : ViewTreeObserver.OnDrawListener {
5050

51-
private val recorder by lazy {
52-
Executors.newSingleThreadScheduledExecutor(RecorderExecutorServiceThreadFactory())
53-
}
5451
private var rootView: WeakReference<View>? = null
5552
private val maskingPaint by lazy(NONE) { Paint() }
5653
private val singlePixelBitmap: Bitmap by lazy(NONE) {
@@ -231,7 +228,6 @@ internal class ScreenshotRecorder(
231228
rootView?.clear()
232229
lastScreenshot?.recycle()
233230
isCapturing.set(false)
234-
recorder.gracefullyShutdown(options)
235231
}
236232

237233
private fun Bitmap.dominantColorForRect(rect: Rect): Int {

sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.sentry.android.replay.util.gracefullyShutdown
88
import io.sentry.android.replay.util.scheduleAtFixedRateSafely
99
import java.lang.ref.WeakReference
1010
import java.util.concurrent.Executors
11+
import java.util.concurrent.ScheduledExecutorService
1112
import java.util.concurrent.ScheduledFuture
1213
import java.util.concurrent.ThreadFactory
1314
import java.util.concurrent.TimeUnit.MILLISECONDS
@@ -17,7 +18,8 @@ import java.util.concurrent.atomic.AtomicBoolean
1718
internal class WindowRecorder(
1819
private val options: SentryOptions,
1920
private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null,
20-
private val mainLooperHandler: MainLooperHandler
21+
private val mainLooperHandler: MainLooperHandler,
22+
private val replayExecutor: ScheduledExecutorService
2123
) : Recorder, OnRootViewsChangedListener {
2224

2325
internal companion object {
@@ -57,7 +59,9 @@ internal class WindowRecorder(
5759
return
5860
}
5961

60-
recorder = ScreenshotRecorder(recorderConfig, options, mainLooperHandler, screenshotRecorderCallback)
62+
recorder = ScreenshotRecorder(recorderConfig, options, mainLooperHandler, replayExecutor, screenshotRecorderCallback)
63+
// TODO: change this to use MainThreadHandler and just post on the main thread with delay
64+
// to avoid thread context switch every time
6165
capturingTask = capturer.scheduleAtFixedRateSafely(
6266
options,
6367
"$TAG.capture",

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.android.replay.capture
22

3+
import android.annotation.TargetApi
34
import android.view.MotionEvent
45
import io.sentry.Breadcrumb
56
import io.sentry.DateUtils
@@ -14,25 +15,22 @@ import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_FRAME_RATE
1415
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_HEIGHT
1516
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_ID
1617
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_ID
17-
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_RECORDING
1818
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_SCREEN_AT_START
1919
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_TYPE
2020
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_TIMESTAMP
2121
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_WIDTH
2222
import io.sentry.android.replay.ScreenshotRecorderConfig
2323
import io.sentry.android.replay.capture.CaptureStrategy.Companion.createSegment
24-
import io.sentry.android.replay.capture.CaptureStrategy.Companion.currentEventsLock
2524
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
2625
import io.sentry.android.replay.gestures.ReplayGestureConverter
27-
import io.sentry.android.replay.util.PersistableLinkedList
28-
import io.sentry.android.replay.util.gracefullyShutdown
2926
import io.sentry.android.replay.util.submitSafely
3027
import io.sentry.protocol.SentryId
3128
import io.sentry.rrweb.RRWebEvent
3229
import io.sentry.transport.ICurrentDateProvider
3330
import java.io.File
3431
import java.util.Date
35-
import java.util.LinkedList
32+
import java.util.Deque
33+
import java.util.concurrent.ConcurrentLinkedDeque
3634
import java.util.concurrent.Executors
3735
import java.util.concurrent.ScheduledExecutorService
3836
import java.util.concurrent.ThreadFactory
@@ -42,11 +40,12 @@ import java.util.concurrent.atomic.AtomicReference
4240
import kotlin.properties.ReadWriteProperty
4341
import kotlin.reflect.KProperty
4442

43+
@TargetApi(26)
4544
internal abstract class BaseCaptureStrategy(
4645
private val options: SentryOptions,
4746
private val hub: IHub?,
4847
private val dateProvider: ICurrentDateProvider,
49-
executor: ScheduledExecutorService? = null,
48+
protected val replayExecutor: ScheduledExecutorService,
5049
private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null
5150
) : CaptureStrategy {
5251

@@ -81,16 +80,8 @@ internal abstract class BaseCaptureStrategy(
8180
override val replayCacheDir: File? get() = cache?.replayCacheDir
8281

8382
override var replayType by persistableAtomic<ReplayType>(propertyName = SEGMENT_KEY_REPLAY_TYPE)
84-
protected val currentEvents: LinkedList<RRWebEvent> = PersistableLinkedList(
85-
propertyName = SEGMENT_KEY_REPLAY_RECORDING,
86-
options,
87-
persistingExecutor,
88-
cacheProvider = { cache }
89-
)
90-
91-
protected val replayExecutor: ScheduledExecutorService by lazy {
92-
executor ?: Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
93-
}
83+
84+
protected val currentEvents: Deque<RRWebEvent> = ConcurrentLinkedDeque()
9485

9586
override fun start(
9687
recorderConfig: ScreenshotRecorderConfig,
@@ -135,7 +126,7 @@ internal abstract class BaseCaptureStrategy(
135126
frameRate: Int = recorderConfig.frameRate,
136127
screenAtStart: String? = this.screenAtStart,
137128
breadcrumbs: List<Breadcrumb>? = null,
138-
events: LinkedList<RRWebEvent> = this.currentEvents
129+
events: Deque<RRWebEvent> = this.currentEvents
139130
): ReplaySegment =
140131
createSegment(
141132
hub,
@@ -161,22 +152,7 @@ internal abstract class BaseCaptureStrategy(
161152
override fun onTouchEvent(event: MotionEvent) {
162153
val rrwebEvents = gestureConverter.convert(event, recorderConfig)
163154
if (rrwebEvents != null) {
164-
synchronized(currentEventsLock) {
165-
currentEvents += rrwebEvents
166-
}
167-
}
168-
}
169-
170-
override fun close() {
171-
replayExecutor.gracefullyShutdown(options)
172-
}
173-
174-
private class ReplayExecutorServiceThreadFactory : ThreadFactory {
175-
private var cnt = 0
176-
override fun newThread(r: Runnable): Thread {
177-
val ret = Thread(r, "SentryReplayIntegration-" + cnt++)
178-
ret.setDaemon(true)
179-
return ret
155+
currentEvents += rrwebEvents
180156
}
181157
}
182158

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.sentry.android.replay.capture
22

3+
import android.annotation.TargetApi
34
import android.graphics.Bitmap
45
import android.view.MotionEvent
56
import io.sentry.DateUtils
@@ -23,14 +24,15 @@ import java.io.File
2324
import java.util.Date
2425
import java.util.concurrent.ScheduledExecutorService
2526

27+
@TargetApi(26)
2628
internal class BufferCaptureStrategy(
2729
private val options: SentryOptions,
2830
private val hub: IHub?,
2931
private val dateProvider: ICurrentDateProvider,
3032
private val random: Random,
31-
executor: ScheduledExecutorService? = null,
33+
executor: ScheduledExecutorService,
3234
replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null
33-
) : BaseCaptureStrategy(options, hub, dateProvider, executor = executor, replayCacheProvider = replayCacheProvider) {
35+
) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider = replayCacheProvider) {
3436

3537
// TODO: capture envelopes for buffered segments instead, but don't send them until buffer is triggered
3638
private val bufferedSegments = mutableListOf<ReplaySegment.Created>()

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.sentry.rrweb.RRWebMetaEvent
1919
import io.sentry.rrweb.RRWebVideoEvent
2020
import java.io.File
2121
import java.util.Date
22+
import java.util.Deque
2223
import java.util.LinkedList
2324

2425
internal interface CaptureStrategy {
@@ -53,11 +54,8 @@ internal interface CaptureStrategy {
5354

5455
fun convert(): CaptureStrategy
5556

56-
fun close()
57-
5857
companion object {
5958
private const val BREADCRUMB_START_OFFSET = 100L
60-
internal val currentEventsLock = Any()
6159

6260
fun createSegment(
6361
hub: IHub?,
@@ -73,7 +71,7 @@ internal interface CaptureStrategy {
7371
frameRate: Int,
7472
screenAtStart: String?,
7573
breadcrumbs: List<Breadcrumb>?,
76-
events: LinkedList<RRWebEvent>
74+
events: Deque<RRWebEvent>
7775
): ReplaySegment {
7876
val generatedVideo = cache?.createVideoOf(
7977
duration,
@@ -127,7 +125,7 @@ internal interface CaptureStrategy {
127125
replayType: ReplayType,
128126
screenAtStart: String?,
129127
breadcrumbs: List<Breadcrumb>,
130-
events: LinkedList<RRWebEvent>
128+
events: Deque<RRWebEvent>
131129
): ReplaySegment {
132130
val endTimestamp = DateUtils.getDateTime(segmentTimestamp.time + videoDuration)
133131
val replay = SentryReplayEvent().apply {
@@ -207,16 +205,16 @@ internal interface CaptureStrategy {
207205
}
208206

209207
internal fun rotateEvents(
210-
events: LinkedList<RRWebEvent>,
208+
events: Deque<RRWebEvent>,
211209
until: Long,
212210
callback: ((RRWebEvent) -> Unit)? = null
213211
) {
214-
synchronized(currentEventsLock) {
215-
var event = events.peek()
216-
while (event != null && event.timestamp < until) {
212+
val iter = events.iterator()
213+
while (iter.hasNext()) {
214+
val event = iter.next()
215+
if (event.timestamp < until) {
217216
callback?.invoke(event)
218-
events.remove()
219-
event = events.peek()
217+
iter.remove()
220218
}
221219
}
222220
}

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class SessionCaptureStrategy(
2020
private val options: SentryOptions,
2121
private val hub: IHub?,
2222
private val dateProvider: ICurrentDateProvider,
23-
executor: ScheduledExecutorService? = null,
23+
executor: ScheduledExecutorService,
2424
replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null
2525
) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider) {
2626

sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/ReplayGestureConverter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class ReplayGestureConverter(
5656

5757
val totalOffset = now - touchMoveBaseline
5858
return if (totalOffset > CAPTURE_MOVE_EVENT_THRESHOLD) {
59-
val moveEvents = mutableListOf<RRWebInteractionMoveEvent>()
59+
val moveEvents = ArrayList<RRWebInteractionMoveEvent>(currentPositions.size)
6060
for ((pointerId, positions) in currentPositions) {
6161
if (positions.isNotEmpty()) {
6262
moveEvents += RRWebInteractionMoveEvent().apply {
@@ -88,7 +88,7 @@ class ReplayGestureConverter(
8888
}
8989

9090
// new finger down - add a new pointer for tracking movement
91-
currentPositions[pId] = ArrayList()
91+
currentPositions[pId] = ArrayList(10)
9292
listOf(
9393
RRWebInteractionEvent().apply {
9494
timestamp = dateProvider.currentTimeMillis

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ internal fun ExecutorService.submitSafely(
5050
taskName: String,
5151
task: Runnable
5252
): Future<*>? {
53+
if (Thread.currentThread().name.startsWith("SentryReplayIntegration")) {
54+
// we're already on the worker thread, no need to submit
55+
task.run()
56+
return null
57+
}
5358
return try {
5459
submit {
5560
try {

0 commit comments

Comments
 (0)