diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt index f69455f2c2c7..2a13a5e5b5a4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt @@ -38,12 +38,35 @@ internal class FrameTimingsObserver( @Volatile private var currentWindow: Window? = null - private val frameMetricsListener = - Window.OnFrameMetricsAvailableListener { _, frameMetrics, _dropCount -> - val beginTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP) - val endTimestamp = beginTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) - emitFrameTiming(beginTimestamp, endTimestamp) - } + fun start() { + if (!isSupported) { + return + } + + frameCounter = 0 + isStarted = true + + // Capture initial screenshot to ensure there's always at least one frame + // recorded at the start of tracing, even if no UI changes occur + val timestamp = System.nanoTime() + emitFrameTiming(timestamp, timestamp) + + currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler) + } + + fun stop() { + if (!isSupported) { + return + } + + isStarted = false + + currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener) + handler.removeCallbacksAndMessages(null) + + bitmapBuffer?.recycle() + bitmapBuffer = null + } fun setCurrentWindow(window: Window?) { if (!isSupported || currentWindow === window) { @@ -57,6 +80,32 @@ internal class FrameTimingsObserver( } } + private val frameMetricsListener = + Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ -> + val beginTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP) + val endTimestamp = beginTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) + emitFrameTiming(beginTimestamp, endTimestamp) + } + + private fun emitFrameTiming(beginTimestamp: Long, endTimestamp: Long) { + val frameId = frameCounter++ + val threadId = Process.myTid() + + CoroutineScope(Dispatchers.Default).launch { + val screenshot = if (screenshotsEnabled) captureScreenshot() else null + + onFrameTimingSequence( + FrameTimingSequence( + frameId, + threadId, + beginTimestamp, + endTimestamp, + screenshot, + ) + ) + } + } + private suspend fun captureScreenshot(): String? = suspendCoroutine { continuation -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { continuation.resume(null) @@ -75,14 +124,11 @@ internal class FrameTimingsObserver( // Reuse bitmap if dimensions haven't changed val bitmap = - bitmapBuffer?.let { - if (it.width == width && it.height == height) { - it - } else { - it.recycle() - null - } - } ?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { bitmapBuffer = it } + bitmapBuffer?.takeIf { it.width == width && it.height == height } + ?: run { + bitmapBuffer?.recycle() + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { bitmapBuffer = it } + } PixelCopy.request( window, @@ -92,19 +138,19 @@ internal class FrameTimingsObserver( CoroutineScope(Dispatchers.Default).launch { var scaledBitmap: Bitmap? = null try { - val scaleFactor = 0.25f - val scaledWidth = (width * scaleFactor).toInt() - val scaledHeight = (height * scaleFactor).toInt() + val density = window.context.resources.displayMetrics.density + val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR).toInt() + val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR).toInt() scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true) val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY - else Bitmap.CompressFormat.WEBP + else Bitmap.CompressFormat.JPEG val base64 = ByteArrayOutputStream().use { outputStream -> - scaledBitmap.compress(compressFormat, 0, outputStream) + scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream) Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) } @@ -123,52 +169,8 @@ internal class FrameTimingsObserver( ) } - fun start() { - if (!isSupported) { - return - } - - frameCounter = 0 - isStarted = true - - // Capture initial screenshot to ensure there's always at least one frame - // recorded at the start of tracing, even if no UI changes occur - val timestamp = System.nanoTime() - emitFrameTiming(timestamp, timestamp) - - currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, handler) - } - - private fun emitFrameTiming(beginTimestamp: Long, endTimestamp: Long) { - val frameId = frameCounter++ - val threadId = Process.myTid() - - CoroutineScope(Dispatchers.Default).launch { - val screenshot = if (screenshotsEnabled) captureScreenshot() else null - - onFrameTimingSequence( - FrameTimingSequence( - frameId, - threadId, - beginTimestamp, - endTimestamp, - screenshot, - ) - ) - } - } - - fun stop() { - if (!isSupported) { - return - } - - isStarted = false - - currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener) - handler.removeCallbacksAndMessages(null) - - bitmapBuffer?.recycle() - bitmapBuffer = null + companion object { + private const val SCREENSHOT_SCALE_FACTOR = 0.75f + private const val SCREENSHOT_QUALITY = 80 } }