Skip to content

Commit d552f2a

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Fix FrameTimingsObverver to initiate PixelCopy on main thread (#55744)
Summary: Pull Request resolved: #55744 Previously, screenshot capture was initiated on a background thread, so `PixelCopy` could run during a later frame than the one being reported. Screenshots now correctly correspond to their frame metrics callback. Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D94368259 fbshipit-source-id: 66af57935a56191a2824e746770e3dbdd7439b33
1 parent e5f9f5f commit d552f2a

1 file changed

Lines changed: 51 additions & 48 deletions

File tree

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

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import android.view.PixelCopy
1818
import android.view.Window
1919
import com.facebook.proguard.annotations.DoNotStripAny
2020
import java.io.ByteArrayOutputStream
21-
import kotlin.coroutines.resume
22-
import kotlin.coroutines.suspendCoroutine
2321
import kotlinx.coroutines.CoroutineScope
2422
import kotlinx.coroutines.Dispatchers
2523
import kotlinx.coroutines.launch
@@ -31,7 +29,7 @@ internal class FrameTimingsObserver(
3129
) {
3230
private val isSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
3331

34-
private val handler = Handler(Looper.getMainLooper())
32+
private val mainHandler = Handler(Looper.getMainLooper())
3533
private var frameCounter: Int = 0
3634
private var bitmapBuffer: Bitmap? = null
3735
private var isStarted: Boolean = false
@@ -51,7 +49,7 @@ internal class FrameTimingsObserver(
5149
val timestamp = System.nanoTime()
5250
emitFrameTiming(timestamp, timestamp)
5351

54-
currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler)
52+
currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler)
5553
}
5654

5755
fun stop() {
@@ -62,7 +60,7 @@ internal class FrameTimingsObserver(
6260
isStarted = false
6361

6462
currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener)
65-
handler.removeCallbacksAndMessages(null)
63+
mainHandler.removeCallbacksAndMessages(null)
6664

6765
bitmapBuffer?.recycle()
6866
bitmapBuffer = null
@@ -76,7 +74,7 @@ internal class FrameTimingsObserver(
7674
currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener)
7775
currentWindow = window
7876
if (isStarted) {
79-
currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler)
77+
currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler)
8078
}
8179
}
8280

@@ -91,31 +89,36 @@ internal class FrameTimingsObserver(
9189
val frameId = frameCounter++
9290
val threadId = Process.myTid()
9391

94-
CoroutineScope(Dispatchers.Default).launch {
95-
val screenshot = if (screenshotsEnabled) captureScreenshot() else null
96-
97-
onFrameTimingSequence(
98-
FrameTimingSequence(
99-
frameId,
100-
threadId,
101-
beginTimestamp,
102-
endTimestamp,
103-
screenshot,
92+
if (screenshotsEnabled) {
93+
// Initiate PixelCopy immediately on the main thread, while still in the current frame,
94+
// then process and emit asynchronously once the copy is complete.
95+
captureScreenshot { screenshot ->
96+
CoroutineScope(Dispatchers.Default).launch {
97+
onFrameTimingSequence(
98+
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot)
10499
)
105-
)
100+
}
101+
}
102+
} else {
103+
CoroutineScope(Dispatchers.Default).launch {
104+
onFrameTimingSequence(
105+
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, null)
106+
)
107+
}
106108
}
107109
}
108110

109-
private suspend fun captureScreenshot(): String? = suspendCoroutine { continuation ->
111+
// Must be called from the main thread so that PixelCopy captures the current frame.
112+
private fun captureScreenshot(callback: (String?) -> Unit) {
110113
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
111-
continuation.resume(null)
112-
return@suspendCoroutine
114+
callback(null)
115+
return
113116
}
114117

115118
val window = currentWindow
116119
if (window == null) {
117-
continuation.resume(null)
118-
return@suspendCoroutine
120+
callback(null)
121+
return
119122
}
120123

121124
val decorView = window.decorView
@@ -136,39 +139,39 @@ internal class FrameTimingsObserver(
136139
{ copyResult ->
137140
if (copyResult == PixelCopy.SUCCESS) {
138141
CoroutineScope(Dispatchers.Default).launch {
139-
var scaledBitmap: Bitmap? = null
140-
try {
141-
val density = window.context.resources.displayMetrics.density
142-
val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR).toInt()
143-
val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR).toInt()
144-
scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true)
145-
146-
val compressFormat =
147-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
148-
Bitmap.CompressFormat.WEBP_LOSSY
149-
else Bitmap.CompressFormat.JPEG
150-
151-
val base64 =
152-
ByteArrayOutputStream().use { outputStream ->
153-
scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream)
154-
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
155-
}
156-
157-
continuation.resume(base64)
158-
} catch (e: Exception) {
159-
continuation.resume(null)
160-
} finally {
161-
scaledBitmap?.recycle()
162-
}
142+
callback(encodeScreenshot(window, bitmap, width, height))
163143
}
164144
} else {
165-
continuation.resume(null)
145+
callback(null)
166146
}
167147
},
168-
handler,
148+
mainHandler,
169149
)
170150
}
171151

152+
private fun encodeScreenshot(window: Window, bitmap: Bitmap, width: Int, height: Int): String? {
153+
var scaledBitmap: Bitmap? = null
154+
return try {
155+
val density = window.context.resources.displayMetrics.density
156+
val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR).toInt()
157+
val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR).toInt()
158+
scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true)
159+
160+
val compressFormat =
161+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY
162+
else Bitmap.CompressFormat.JPEG
163+
164+
ByteArrayOutputStream().use { outputStream ->
165+
scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream)
166+
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
167+
}
168+
} catch (e: Exception) {
169+
null
170+
} finally {
171+
scaledBitmap?.recycle()
172+
}
173+
}
174+
172175
companion object {
173176
private const val SCREENSHOT_SCALE_FACTOR = 0.75f
174177
private const val SCREENSHOT_QUALITY = 80

0 commit comments

Comments
 (0)