Skip to content

Commit 9ff647c

Browse files
huntiefacebook-github-bot
authored andcommitted
Add dynamic sampling to frame screenshots
Summary: Update Android frame screenshot processing to skip frames when encoding is backed up, ensuring we capture at most every 5th frame when pending work exists. **Motivation**: Prevents truncated trace data on slower devices (e.g. missing screenshots for the last 1/3 of the trace), with the tradeoff of some intermediate frame data loss (note: we **do** emit a frame event, just without a visual snapshot). NOTE: This still does not yet solve crashes (OkHttp network chunk size overflow) for heavy frame data at a high FPS on fast devices (coming next). **Throttling logic** - Each frame increments `framesSinceLastEnqueue`. - If there are pending frames AND we haven't reached the minimum sample rate (5), the frame is skipped. - When a frame is enqueued, reset the counter and track it as pending. - When processing completes (in either screenshot or non-screenshot path), decrement the pending count. **🚧 Still under experimentation** A `MIN_FRAME_SAMPLE_RATE` of 5 may be too destructive, in which case we will evolve this over time. NOTE: ⚠️ We do still typically loose a small region of pending frames at the end of a trace, but this is now more reasonable. A further fix (likely unnecessary) is to add the lifecycle methods to await frame processing before responding over CDP. Changelog: [Internal] Differential Revision: D95987488
1 parent 19350b1 commit 9ff647c

1 file changed

Lines changed: 27 additions & 4 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import android.view.PixelCopy
1717
import android.view.Window
1818
import com.facebook.proguard.annotations.DoNotStripAny
1919
import java.io.ByteArrayOutputStream
20+
import java.util.concurrent.atomic.AtomicInteger
2021
import kotlinx.coroutines.CoroutineScope
2122
import kotlinx.coroutines.Dispatchers
2223
import kotlinx.coroutines.launch
@@ -30,6 +31,8 @@ internal class FrameTimingsObserver(
3031
private val mainHandler = Handler(Looper.getMainLooper())
3132

3233
private var frameCounter: Int = 0
34+
private var framesSinceLastEnqueue: Int = 0
35+
private val pendingFrameCount = AtomicInteger(0)
3336
@Volatile private var isTracing: Boolean = false
3437
@Volatile private var currentWindow: Window? = null
3538

@@ -39,6 +42,8 @@ internal class FrameTimingsObserver(
3942
}
4043

4144
frameCounter = 0
45+
framesSinceLastEnqueue = 0
46+
pendingFrameCount.set(0)
4247
isTracing = true
4348

4449
// Capture initial screenshot to ensure there's always at least one frame
@@ -86,19 +91,34 @@ internal class FrameTimingsObserver(
8691

8792
private fun emitFrameTiming(beginTimestamp: Long, endTimestamp: Long) {
8893
val frameId = frameCounter++
94+
framesSinceLastEnqueue++
95+
8996
val threadId = Process.myTid()
9097

91-
if (screenshotsEnabled) {
98+
// Throttle screenshot capture if we have pending frames and haven't reached minimum sample rate
99+
val shouldCaptureScreenshot =
100+
screenshotsEnabled &&
101+
(pendingFrameCount.get() == 0 || framesSinceLastEnqueue >= MIN_FRAME_SAMPLE_RATE)
102+
103+
if (shouldCaptureScreenshot) {
104+
framesSinceLastEnqueue = 0
105+
pendingFrameCount.incrementAndGet()
106+
92107
// Initiate PixelCopy immediately on the main thread, while still in the current frame,
93108
// then process and emit asynchronously once the copy is complete.
94109
captureScreenshot { screenshot ->
95110
CoroutineScope(Dispatchers.Default).launch {
96-
onFrameTimingSequence(
97-
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot)
98-
)
111+
try {
112+
onFrameTimingSequence(
113+
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot)
114+
)
115+
} finally {
116+
pendingFrameCount.decrementAndGet()
117+
}
99118
}
100119
}
101120
} else {
121+
// Emit frame timing without screenshot (either screenshots disabled or when throttled)
102122
CoroutineScope(Dispatchers.Default).launch {
103123
onFrameTimingSequence(
104124
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, null)
@@ -172,6 +192,9 @@ internal class FrameTimingsObserver(
172192
}
173193

174194
companion object {
195+
// Minimum frame sample rate - ensures we enqueue at least every 5 frames
196+
private const val MIN_FRAME_SAMPLE_RATE = 5
197+
175198
private const val SCREENSHOT_SCALE_FACTOR = 0.75f
176199
private const val SCREENSHOT_QUALITY = 80
177200

0 commit comments

Comments
 (0)