Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR] Flutter improvements #4007

Merged
merged 11 commits into from
Dec 20, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

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

### Internal

- Session Replay: Flutter improvements ([#4007](https://github.com/getsentry/sentry-java/pull/4007))

## 7.19.0

Expand Down
10 changes: 5 additions & 5 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos
public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public static final field $stable I
public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion;
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)V
public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V
public static synthetic fun addFrame$default (Lio/sentry/android/replay/ReplayCache;Ljava/io/File;JLjava/lang/String;ILjava/lang/Object;)V
public fun close ()V
public final fun createVideoOf (JJIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
public final fun createVideoOf (JJIIIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
public final fun persistSegmentValues (Ljava/lang/String;Ljava/lang/String;)V
public final fun rotate (J)Ljava/lang/String;
}
Expand All @@ -60,8 +60,8 @@ public final class io/sentry/android/replay/ReplayCache$Companion {
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
public static final field $stable I
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun captureReplay (Ljava/lang/Boolean;)V
public fun close ()V
public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
public class ReplayCache(
private val options: SentryOptions,
private val replayId: SentryId,
private val recorderConfig: ScreenshotRecorderConfig
private val replayId: SentryId
) : Closeable {

private val isClosed = AtomicBoolean(false)
Expand Down Expand Up @@ -133,6 +132,8 @@ public class ReplayCache(
segmentId: Int,
height: Int,
width: Int,
frameRate: Int,
bitRate: Int,
videoFile: File = File(replayCacheDir, "$segmentId.mp4")
): GeneratedVideo? {
if (videoFile.exists() && videoFile.length() > 0) {
Expand All @@ -146,21 +147,20 @@ public class ReplayCache(
return null
}

// TODO: reuse instance of encoder and just change file path to create a different muxer
encoder = synchronized(encoderLock) {
SimpleVideoEncoder(
options,
MuxerConfig(
file = videoFile,
recordingHeight = height,
recordingWidth = width,
frameRate = recorderConfig.frameRate,
bitRate = recorderConfig.bitRate
frameRate = frameRate,
bitRate = bitRate
)
).also { it.start() }
}

val step = 1000 / recorderConfig.frameRate.toLong()
val step = 1000 / frameRate.toLong()
var frameCount = 0
var lastFrame: ReplayFrame = frames.first()
for (timestamp in from until (from + (duration)) step step) {
Expand Down Expand Up @@ -306,7 +306,7 @@ public class ReplayCache(
}
}

internal fun fromDisk(options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null): LastSegmentData? {
internal fun fromDisk(options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null): LastSegmentData? {
val replayCacheDir = makeReplayCacheDir(options, replayId)
val lastSegmentFile = File(replayCacheDir, ONGOING_SEGMENT)
if (!lastSegmentFile.exists()) {
Expand Down Expand Up @@ -360,7 +360,7 @@ public class ReplayCache(
scaleFactorY = 1.0f
)

val cache = replayCacheProvider?.invoke(replayId, recorderConfig) ?: ReplayCache(options, replayId, recorderConfig)
val cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId)
cache.replayCacheDir?.listFiles { dir, name ->
if (name.endsWith(".jpg")) {
val file = File(dir, name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.sentry.android.replay.gestures.GestureRecorder
import io.sentry.android.replay.gestures.TouchRecorderCallback
import io.sentry.android.replay.util.MainLooperHandler
import io.sentry.android.replay.util.appContext
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.sample
import io.sentry.android.replay.util.submitSafely
import io.sentry.cache.PersistingScopeObserver
Expand All @@ -46,15 +47,16 @@ import io.sentry.util.Random
import java.io.Closeable
import java.io.File
import java.util.LinkedList
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.LazyThreadSafetyMode.NONE

public class ReplayIntegration(
private val context: Context,
private val dateProvider: ICurrentDateProvider,
private val recorderProvider: (() -> Recorder)? = null,
private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null,
private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null
) : Integration,
Closeable,
ScreenshotRecorderCallback,
Expand All @@ -78,7 +80,7 @@ public class ReplayIntegration(
dateProvider: ICurrentDateProvider,
recorderProvider: (() -> Recorder)?,
recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)?,
replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)?,
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)?,
replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null,
mainLooperHandler: MainLooperHandler? = null,
gestureRecorderProvider: (() -> GestureRecorder)? = null
Expand All @@ -93,7 +95,10 @@ public class ReplayIntegration(
private var recorder: Recorder? = null
private var gestureRecorder: GestureRecorder? = null
private val random by lazy { Random() }
internal val rootViewsSpy by lazy(NONE) { RootViewsSpy.install() }
internal val rootViewsSpy by lazy { RootViewsSpy.install() }
private val replayExecutor by lazy {
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
}

// TODO: probably not everything has to be thread-safe here
internal val isEnabled = AtomicBoolean(false)
Expand All @@ -105,8 +110,6 @@ public class ReplayIntegration(
private var mainLooperHandler: MainLooperHandler = MainLooperHandler()
private var gestureRecorderProvider: (() -> GestureRecorder)? = null

private lateinit var recorderConfig: ScreenshotRecorderConfig

override fun register(hub: IHub, options: SentryOptions) {
this.options = options

Expand All @@ -123,16 +126,22 @@ public class ReplayIntegration(
}

this.hub = hub
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler)
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor)
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
isEnabled.set(true)

options.connectionStatusProvider.addConnectionStatusObserver(this)
hub.rateLimiter?.addRateLimitObserver(this)
try {
context.registerComponentCallbacks(this)
} catch (e: Throwable) {
options.logger.log(INFO, "ComponentCallbacks is not available, orientation changes won't be handled by Session replay", e)
if (options.experimental.sessionReplay.isTrackOrientationChange) {
try {
context.registerComponentCallbacks(this)
} catch (e: Throwable) {
options.logger.log(
INFO,
"ComponentCallbacks is not available, orientation changes won't be handled by Session replay",
e
)
}
}

addIntegrationToSdkVersion("Replay")
Expand Down Expand Up @@ -164,11 +173,11 @@ public class ReplayIntegration(
return
}

recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
SessionCaptureStrategy(options, hub, dateProvider, replayCacheProvider = replayCacheProvider)
SessionCaptureStrategy(options, hub, dateProvider, replayExecutor, replayCacheProvider)
} else {
BufferCaptureStrategy(options, hub, dateProvider, random, replayCacheProvider = replayCacheProvider)
BufferCaptureStrategy(options, hub, dateProvider, random, replayExecutor, replayCacheProvider)
}

captureStrategy?.start(recorderConfig)
Expand Down Expand Up @@ -229,7 +238,6 @@ public class ReplayIntegration(
gestureRecorder?.stop()
captureStrategy?.stop()
isRecording.set(false)
captureStrategy?.close()
captureStrategy = null
}

Expand All @@ -256,14 +264,17 @@ public class ReplayIntegration(

options.connectionStatusProvider.removeConnectionStatusObserver(this)
hub?.rateLimiter?.removeRateLimitObserver(this)
try {
context.unregisterComponentCallbacks(this)
} catch (ignored: Throwable) {
if (options.experimental.sessionReplay.isTrackOrientationChange) {
try {
context.unregisterComponentCallbacks(this)
} catch (ignored: Throwable) {
}
}
stop()
recorder?.close()
recorder = null
rootViewsSpy.close()
replayExecutor.gracefullyShutdown(options)
}

override fun onConfigurationChanged(newConfig: Configuration) {
Expand All @@ -274,7 +285,7 @@ public class ReplayIntegration(
recorder?.stop()

// refresh config based on new device configuration
recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
captureStrategy?.onConfigurationChanged(recorderConfig)

recorder?.start(recorderConfig)
Expand Down Expand Up @@ -387,6 +398,7 @@ public class ReplayIntegration(
height = lastSegment.recorderConfig.recordingHeight,
width = lastSegment.recorderConfig.recordingWidth,
frameRate = lastSegment.recorderConfig.frameRate,
bitRate = lastSegment.recorderConfig.bitRate,
cache = lastSegment.cache,
replayType = lastSegment.replayType,
screenAtStart = lastSegment.screenAtStart,
Expand All @@ -405,4 +417,13 @@ public class ReplayIntegration(
private class PreviousReplayHint : Backfillable {
override fun shouldEnrich(): Boolean = false
}

private class ReplayExecutorServiceThreadFactory : ThreadFactory {
private var cnt = 0
override fun newThread(r: Runnable): Thread {
val ret = Thread(r, "SentryReplayIntegration-" + cnt++)
ret.setDaemon(true)
return ret
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import io.sentry.SentryReplayOptions
import io.sentry.android.replay.util.MainLooperHandler
import io.sentry.android.replay.util.addOnDrawListenerSafe
import io.sentry.android.replay.util.getVisibleRects
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.removeOnDrawListenerSafe
import io.sentry.android.replay.util.submitSafely
import io.sentry.android.replay.util.traverse
Expand All @@ -34,7 +33,7 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarc
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.LazyThreadSafetyMode.NONE
Expand All @@ -44,13 +43,11 @@ import kotlin.math.roundToInt
internal class ScreenshotRecorder(
val config: ScreenshotRecorderConfig,
val options: SentryOptions,
val mainLooperHandler: MainLooperHandler,
private val mainLooperHandler: MainLooperHandler,
private val recorder: ScheduledExecutorService,
private val screenshotRecorderCallback: ScreenshotRecorderCallback?
) : ViewTreeObserver.OnDrawListener {

private val recorder by lazy {
Executors.newSingleThreadScheduledExecutor(RecorderExecutorServiceThreadFactory())
}
private var rootView: WeakReference<View>? = null
private val maskingPaint by lazy(NONE) { Paint() }
private val singlePixelBitmap: Bitmap by lazy(NONE) {
Expand Down Expand Up @@ -231,7 +228,6 @@ internal class ScreenshotRecorder(
rootView?.clear()
lastScreenshot?.recycle()
isCapturing.set(false)
recorder.gracefullyShutdown(options)
}

private fun Bitmap.dominantColorForRect(rect: Rect): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.scheduleAtFixedRateSafely
import java.lang.ref.WeakReference
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit.MILLISECONDS
Expand All @@ -17,7 +18,8 @@ import java.util.concurrent.atomic.AtomicBoolean
internal class WindowRecorder(
private val options: SentryOptions,
private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null,
private val mainLooperHandler: MainLooperHandler
private val mainLooperHandler: MainLooperHandler,
private val replayExecutor: ScheduledExecutorService
) : Recorder, OnRootViewsChangedListener {

internal companion object {
Expand Down Expand Up @@ -57,7 +59,9 @@ internal class WindowRecorder(
return
}

recorder = ScreenshotRecorder(recorderConfig, options, mainLooperHandler, screenshotRecorderCallback)
recorder = ScreenshotRecorder(recorderConfig, options, mainLooperHandler, replayExecutor, screenshotRecorderCallback)
// TODO: change this to use MainThreadHandler and just post on the main thread with delay
// to avoid thread context switch every time
capturingTask = capturer.scheduleAtFixedRateSafely(
options,
"$TAG.capture",
Expand Down
Loading
Loading