Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
}

Expand All @@ -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
}
}
Loading