Skip to content

Commit

Permalink
Cleanup and enrich collected metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
murki committed Nov 12, 2024
1 parent 4f6279e commit 66ec104
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.common.Runtime
import io.bitdrift.capture.common.RuntimeFeature
import io.bitdrift.capture.providers.FieldValue
import io.bitdrift.capture.providers.toFieldValue
import io.bitdrift.capture.providers.toFields
import io.bitdrift.capture.replay.ReplayCaptureController
import io.bitdrift.capture.replay.SessionReplayController
import io.bitdrift.capture.replay.IReplayLogger
import io.bitdrift.capture.replay.IScreenshotLogger
import io.bitdrift.capture.replay.SessionReplayConfiguration
import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
import io.bitdrift.capture.replay.ReplayCaptureMetrics
import io.bitdrift.capture.replay.ScreenshotCaptureMetrics
import io.bitdrift.capture.replay.internal.FilteredCapture
import kotlin.time.Duration

// Controls the replay feature
internal class SessionReplayTarget(
Expand All @@ -40,7 +39,7 @@ internal class SessionReplayTarget(
// `sessionReplayTarget` argument is moved from logger creation time to logger start time.
// Refer to TODO in `LoggerImpl` for more details.
internal var runtime: Runtime? = null
private val replayCaptureController: ReplayCaptureController = ReplayCaptureController(
private val sessionReplayController: SessionReplayController = SessionReplayController(
errorHandler,
this,
this,
Expand All @@ -54,10 +53,10 @@ internal class SessionReplayTarget(
runtime?.isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE)
?: RuntimeFeature.SESSION_REPLAY_COMPOSE.defaultValue
)
replayCaptureController.captureScreen(skipReplayComposeViews)
sessionReplayController.captureScreen(skipReplayComposeViews)
}

override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) {
override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics) {
val fields = buildMap {
put("screen", encodedScreen.toFieldValue())
putAll(metrics.toMap().toFields())
Expand All @@ -69,13 +68,14 @@ internal class SessionReplayTarget(
override fun captureScreenshot() {
// TODO(murki): Gate behind Runtime flag
Log.i("miguel-Screenshot", "captureScreenshot is being implemented on Android")
replayCaptureController.captureScreenshot()
sessionReplayController.captureScreenshot()
}

override fun onScreenshotCaptured(compressedScreen: ByteArray, durationMs: Long) {
override fun onScreenshotCaptured(compressedScreen: ByteArray, metrics: ScreenshotCaptureMetrics) {
val allFields = buildMap {
put("screen_px", compressedScreen.toFieldValue())
put("_duration_ms", durationMs.toString().toFieldValue())
putAll(metrics.toMap().toFields())
put("_duration_ms", metrics.screenshotTimeMs.toString().toFieldValue())
}
// TODO(murki): Migrate to call rust logger.log_session_replay_screenshot()
logger.log(LogType.REPLAY, LogLevel.INFO, allFields) { "Screenshot captured" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

package io.bitdrift.capture.replay

import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
import io.bitdrift.capture.replay.internal.FilteredCapture

/**
Expand All @@ -20,7 +19,7 @@ interface IReplayLogger {
* @param screen The list of captured elements after filtering
* @param metrics Metrics about the screen capture
*/
fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics)
fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics)

/**
* Forwards a verbose message internally to the SDK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package io.bitdrift.capture.replay
import kotlin.time.Duration

interface IScreenshotLogger {
fun onScreenshotCaptured(compressedScreen: ByteArray, durationMs: Long)
fun onScreenshotCaptured(compressedScreen: ByteArray, metrics: ScreenshotCaptureMetrics)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture.replay.internal
package io.bitdrift.capture.replay

import kotlin.time.Duration

Expand All @@ -17,32 +17,34 @@ import kotlin.time.Duration
* @param exceptionCausingViewCount The number of views that caused an exception during capture
* @param viewCountAfterFilter The number of views after filtering
* @param parseDuration The time it took to parse the view tree
* @param captureTimeMs The total time it took to capture the screen (parse + encoding)
* @param encodingTimeMs The time it took to encode all replay elements
*/
data class EncodedScreenMetrics(
data class ReplayCaptureMetrics(
var viewCount: Int = 0,
var composeViewCount: Int = 0,
var errorViewCount: Int = 0,
var exceptionCausingViewCount: Int = 0,
var viewCountAfterFilter: Int = 0,
var parseDuration: Duration = Duration.ZERO,
var captureTimeMs: Long = 0L,
var encodingTimeMs: Long = 0L,
) {

private val totalDurationMs: Long
get() = parseDuration.inWholeMilliseconds + encodingTimeMs

/**
* Convert the metrics to a map
*/
fun toMap(): Map<String, String> {
/**
* 'parseTime' is not included in the output map as it's passed to the Rust layer separately.
*/
return mapOf(
"viewCount" to viewCount.toString(),
"composeViewCount" to composeViewCount.toString(),
"viewCountAfterFilter" to viewCountAfterFilter.toString(),
"errorViewCount" to errorViewCount.toString(),
"exceptionCausingViewCount" to exceptionCausingViewCount.toString(),
"captureTimeMs" to captureTimeMs.toString(),
"view_count" to viewCount.toString(),
"compose_view_count" to composeViewCount.toString(),
"view_count_after_filter" to viewCountAfterFilter.toString(),
"error_view_count" to errorViewCount.toString(),
"exception_causing_view_count" to exceptionCausingViewCount.toString(),
"parse_duration_ms" to parseDuration.inWholeMilliseconds.toString(),
"encoding_time_ms" to encodingTimeMs.toString(),
"total_duration_ms" to totalDurationMs.toString(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import android.util.Log
import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.replay.internal.DisplayManagers
import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
import io.bitdrift.capture.replay.internal.FilteredCapture
import io.bitdrift.capture.replay.internal.ReplayCaptureEngine
import io.bitdrift.capture.replay.internal.WindowManager
Expand All @@ -25,7 +24,6 @@ import okhttp3.WebSocketListener
import okio.ByteString
import okio.ByteString.Companion.toByteString
import java.util.concurrent.TimeUnit
import kotlin.time.Duration

/**
* Allows to capture the screen and send the binary data over a persistent websocket connection
Expand Down Expand Up @@ -95,7 +93,7 @@ class ReplayPreviewClient(
}
}

override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) {
override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics) {
lastEncodedScreen = encodedScreen
webSocket?.send(encodedScreen.toByteString(0, encodedScreen.size))
// forward the callback to the module's logger
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.bitdrift.capture.replay

data class ScreenshotCaptureMetrics(
val screenshotTimeMs: Long,
val screenshotAllocationByteCount: Int,
val screenshotByteCount: Int,
var compressionTimeMs: Long = 0,
var compressionByteCount: Int = 0,
) {
val totalDurationMs: Long
get() = screenshotTimeMs + compressionTimeMs

fun toMap(): Map<String, String> {
return mapOf(
"screenshot_time_ms" to screenshotTimeMs.toString(),
"screenshot_allocation_byte_count" to screenshotAllocationByteCount.toString(),
"screenshot_byte_count" to screenshotByteCount.toString(),
"compression_time_ms" to compressionTimeMs.toString(),
"compression_byte_count" to compressionByteCount.toString(),
"total_duration_ms" to totalDurationMs.toString(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.replay.internal.DisplayManagers
import io.bitdrift.capture.replay.internal.ReplayCaptureEngine
import io.bitdrift.capture.replay.internal.ScreenshotCaptureEngine
import io.bitdrift.capture.replay.internal.WindowManager

/**
Expand All @@ -21,7 +22,7 @@ import io.bitdrift.capture.replay.internal.WindowManager
* @param sessionReplayConfiguration the configuration to use
* @param runtime allows for the feature to be remotely disabled
*/
class ReplayCaptureController(
class SessionReplayController(
errorHandler: ErrorHandler,
replayLogger: IReplayLogger,
screenshotLogger: IScreenshotLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import io.bitdrift.capture.common.DefaultClock
import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.IClock
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.replay.ReplayCaptureController
import io.bitdrift.capture.replay.SessionReplayController
import io.bitdrift.capture.replay.IReplayLogger
import io.bitdrift.capture.replay.ReplayCaptureMetrics
import io.bitdrift.capture.replay.SessionReplayConfiguration
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
Expand Down Expand Up @@ -46,24 +47,25 @@ internal class ReplayCaptureEngine(

private fun captureScreen(
skipReplayComposeViews: Boolean,
completion: (encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) -> Unit,
completion: (encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics) -> Unit,
) {
val startTime = clock.elapsedRealtime()

val encodedScreenMetrics = EncodedScreenMetrics()
val replayCaptureMetrics = ReplayCaptureMetrics()
val timedValue = measureTimedValue {
captureParser.parse(encodedScreenMetrics, skipReplayComposeViews)
captureParser.parse(replayCaptureMetrics, skipReplayComposeViews)
}

executor.execute {
captureFilter.filter(timedValue.value)?.let { filteredCapture ->
encodedScreenMetrics.parseDuration = timedValue.duration
encodedScreenMetrics.viewCountAfterFilter = filteredCapture.size
replayCaptureMetrics.parseDuration = timedValue.duration
replayCaptureMetrics.viewCountAfterFilter = filteredCapture.size
val screen = captureDecorations.addDecorations(filteredCapture)
val encodedScreen = replayEncoder.encode(screen)
encodedScreenMetrics.captureTimeMs = clock.elapsedRealtime() - startTime
ReplayCaptureController.L.d("Screen Captured: $encodedScreenMetrics")
completion(encodedScreen, screen, encodedScreenMetrics)
replayCaptureMetrics.encodingTimeMs =
clock.elapsedRealtime() - startTime - replayCaptureMetrics.parseDuration.inWholeMilliseconds
SessionReplayController.L.d("Screen Captured: $replayCaptureMetrics")
completion(encodedScreen, screen, replayCaptureMetrics)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ package io.bitdrift.capture.replay.internal

import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.replay.ReplayCaptureController
import io.bitdrift.capture.replay.SessionReplayController
import io.bitdrift.capture.replay.ReplayType

// Add the screen and keyboard layouts to the replay capture
Expand All @@ -22,7 +21,7 @@ internal class ReplayDecorations(
fun addDecorations(filteredCapture: FilteredCapture): FilteredCapture {
// Add screen size as the first element
val bounds = displayManager.refreshDisplay()
ReplayCaptureController.L.d("Display Screen size $bounds")
SessionReplayController.L.d("Display Screen size $bounds")
val screen: MutableList<ReplayRect> = mutableListOf(bounds)
screen.addAll(filteredCapture)

Expand All @@ -41,7 +40,7 @@ internal class ReplayDecorations(
width = rootView.width,
height = insets.bottom,
)
ReplayCaptureController.L.d("Keyboard IME size $imeBounds")
SessionReplayController.L.d("Keyboard IME size $imeBounds")
screen.add(imeBounds)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
package io.bitdrift.capture.replay.internal

import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.replay.ReplayCaptureController
import io.bitdrift.capture.replay.SessionReplayController
import io.bitdrift.capture.replay.ReplayCaptureMetrics
import io.bitdrift.capture.replay.SessionReplayConfiguration
import io.bitdrift.capture.replay.internal.mappers.ViewMapper

Expand All @@ -24,29 +25,29 @@ internal class ReplayParser(
/**
* Parses a ScannableView tree hierarchy into a list of ReplayRect
*/
fun parse(encodedScreenMetrics: EncodedScreenMetrics, skipReplayComposeViews: Boolean): Capture {
fun parse(replayCaptureMetrics: ReplayCaptureMetrics, skipReplayComposeViews: Boolean): Capture {
val result = mutableListOf<List<ReplayRect>>()

// Use a stack to perform a DFS traversal of the tree and avoid recursion
val stack: ArrayDeque<ScannableView> = ArrayDeque(
windowManager.findRootViews().map {
ReplayCaptureController.L.v("Root view found and added to list: ${it.javaClass.simpleName}")
SessionReplayController.L.v("Root view found and added to list: ${it.javaClass.simpleName}")
ScannableView.AndroidView(it, skipReplayComposeViews)
},
)
while (stack.isNotEmpty()) {
val currentNode = stack.removeLast()
try {
viewMapper.updateMetrics(currentNode, encodedScreenMetrics)
viewMapper.updateMetrics(currentNode, replayCaptureMetrics)
if (!viewMapper.viewIsVisible(currentNode)) {
ReplayCaptureController.L.v("Ignoring not visible view: ${currentNode.displayName}")
SessionReplayController.L.v("Ignoring not visible view: ${currentNode.displayName}")
continue
}
result.add(viewMapper.mapView(currentNode))
} catch (e: Throwable) {
val errorMsg = "Error parsing view, Skipping $currentNode and children"
ReplayCaptureController.L.e(e, errorMsg)
encodedScreenMetrics.exceptionCausingViewCount += 1
SessionReplayController.L.e(e, errorMsg)
replayCaptureMetrics.exceptionCausingViewCount += 1
errorHandler.handleError(errorMsg, e)
}
// Convert the sequence of children to a list to process in reverse order
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.bitdrift.capture.replay
package io.bitdrift.capture.replay.internal

import android.content.Context
import android.graphics.Bitmap
Expand All @@ -9,8 +9,8 @@ import io.bitdrift.capture.common.DefaultClock
import io.bitdrift.capture.common.ErrorHandler
import io.bitdrift.capture.common.IClock
import io.bitdrift.capture.common.MainThreadHandler
import io.bitdrift.capture.replay.internal.DisplayManagers
import io.bitdrift.capture.replay.internal.WindowManager
import io.bitdrift.capture.replay.IScreenshotLogger
import io.bitdrift.capture.replay.ScreenshotCaptureMetrics
import java.io.ByteArrayOutputStream
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
Expand Down Expand Up @@ -38,9 +38,10 @@ internal class ScreenshotCaptureEngine(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return
}
val startTime = clock.elapsedRealtime()
val startTimeMs = clock.elapsedRealtime()
// TODO(murki): Log empty screenshot on unblock the caller if there are no root views
val topView = windowManager.findRootViews().lastOrNull() ?: return
// TODO(murki): Consider calling setDestinationBitmap() with a Bitmap.Config.RGB_565 instead
// TODO(murki): Reduce memory footprint by calling setDestinationBitmap() with a Bitmap.Config.RGB_565 instead
// of the default of Bitmap.Config.ARGB_8888
val screenshotRequest = PixelCopy.Request.Builder.ofWindow(topView).build()
PixelCopy.request(screenshotRequest, executor) { screenshotResult ->
Expand All @@ -50,28 +51,26 @@ internal class ScreenshotCaptureEngine(
return@request
}

val screenshotTimeMs = clock.elapsedRealtime() - startTime
val resultBitmap = screenshotResult.bitmap
Log.d("miguel-Screenshot", "Miguel-PixelCopy finished capture on thread=${Thread.currentThread().name}, " +
"allocationByteCount=${resultBitmap.allocationByteCount}, " +
"byteCount=${resultBitmap.byteCount}, " +
"duration=$screenshotTimeMs")
val metrics = ScreenshotCaptureMetrics(
screenshotTimeMs = clock.elapsedRealtime() - startTimeMs,
screenshotAllocationByteCount = resultBitmap.allocationByteCount,
screenshotByteCount = resultBitmap.byteCount
)
val stream = ByteArrayOutputStream()
// TODO(murki): Confirm the exact compression method used on iOS
// Encode bitmap to bytearray while compressing it using JPEG=10 quality
// Encode bitmap to bytearray while compressing it using JPEG=10 quality to match iOS
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 10, stream)
resultBitmap.recycle()
// TODO (murki): Figure out if there's a more memory efficient way to do this
// see https://stackoverflow.com/questions/4989182/converting-java-bitmap-to-byte-array#comment36547795_4989543 and
// and https://gaumala.com/posts/2020-01-27-working-with-streams-kotlin.html
val screenshotBytes = stream.toByteArray()
val compressDurationMs = clock.elapsedRealtime() - startTime - screenshotTimeMs
val totalDurationMs = compressDurationMs + screenshotTimeMs
Log.d("miguel-Screenshot", "Miguel-Finished compression on thread=${Thread.currentThread().name}, " +
"screenshotBytes.size=${screenshotBytes.size}, " +
"duration=$compressDurationMs, " +
"totalDuration=$totalDurationMs")
logger.onScreenshotCaptured(screenshotBytes, totalDurationMs)
metrics.compressionTimeMs = clock.elapsedRealtime() - startTimeMs - metrics.screenshotTimeMs
metrics.compressionByteCount = screenshotBytes.size
Log.d("miguel-Screenshot", "Miguel-Finished screenshot operation on thread=${Thread.currentThread().name}, " +
"metrics=$metrics"
)
logger.onScreenshotCaptured(screenshotBytes, metrics)
}

}
Expand Down
Loading

0 comments on commit 66ec104

Please sign in to comment.