Skip to content

Commit 66ec104

Browse files
committed
Cleanup and enrich collected metrics
1 parent 4f6279e commit 66ec104

File tree

14 files changed

+107
-83
lines changed

14 files changed

+107
-83
lines changed

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/SessionReplayTarget.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@ import io.bitdrift.capture.common.ErrorHandler
1717
import io.bitdrift.capture.common.MainThreadHandler
1818
import io.bitdrift.capture.common.Runtime
1919
import io.bitdrift.capture.common.RuntimeFeature
20-
import io.bitdrift.capture.providers.FieldValue
2120
import io.bitdrift.capture.providers.toFieldValue
2221
import io.bitdrift.capture.providers.toFields
23-
import io.bitdrift.capture.replay.ReplayCaptureController
22+
import io.bitdrift.capture.replay.SessionReplayController
2423
import io.bitdrift.capture.replay.IReplayLogger
2524
import io.bitdrift.capture.replay.IScreenshotLogger
2625
import io.bitdrift.capture.replay.SessionReplayConfiguration
27-
import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
26+
import io.bitdrift.capture.replay.ReplayCaptureMetrics
27+
import io.bitdrift.capture.replay.ScreenshotCaptureMetrics
2828
import io.bitdrift.capture.replay.internal.FilteredCapture
29-
import kotlin.time.Duration
3029

3130
// Controls the replay feature
3231
internal class SessionReplayTarget(
@@ -40,7 +39,7 @@ internal class SessionReplayTarget(
4039
// `sessionReplayTarget` argument is moved from logger creation time to logger start time.
4140
// Refer to TODO in `LoggerImpl` for more details.
4241
internal var runtime: Runtime? = null
43-
private val replayCaptureController: ReplayCaptureController = ReplayCaptureController(
42+
private val sessionReplayController: SessionReplayController = SessionReplayController(
4443
errorHandler,
4544
this,
4645
this,
@@ -54,10 +53,10 @@ internal class SessionReplayTarget(
5453
runtime?.isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE)
5554
?: RuntimeFeature.SESSION_REPLAY_COMPOSE.defaultValue
5655
)
57-
replayCaptureController.captureScreen(skipReplayComposeViews)
56+
sessionReplayController.captureScreen(skipReplayComposeViews)
5857
}
5958

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

75-
override fun onScreenshotCaptured(compressedScreen: ByteArray, durationMs: Long) {
74+
override fun onScreenshotCaptured(compressedScreen: ByteArray, metrics: ScreenshotCaptureMetrics) {
7675
val allFields = buildMap {
7776
put("screen_px", compressedScreen.toFieldValue())
78-
put("_duration_ms", durationMs.toString().toFieldValue())
77+
putAll(metrics.toMap().toFields())
78+
put("_duration_ms", metrics.screenshotTimeMs.toString().toFieldValue())
7979
}
8080
// TODO(murki): Migrate to call rust logger.log_session_replay_screenshot()
8181
logger.log(LogType.REPLAY, LogLevel.INFO, allFields) { "Screenshot captured" }

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/IReplayLogger.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
package io.bitdrift.capture.replay
99

10-
import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
1110
import io.bitdrift.capture.replay.internal.FilteredCapture
1211

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

2524
/**
2625
* Forwards a verbose message internally to the SDK

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/IScreenshotLogger.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ package io.bitdrift.capture.replay
33
import kotlin.time.Duration
44

55
interface IScreenshotLogger {
6-
fun onScreenshotCaptured(compressedScreen: ByteArray, durationMs: Long)
6+
fun onScreenshotCaptured(compressedScreen: ByteArray, metrics: ScreenshotCaptureMetrics)
77
}
+15-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// LICENSE file or at:
66
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
77

8-
package io.bitdrift.capture.replay.internal
8+
package io.bitdrift.capture.replay
99

1010
import kotlin.time.Duration
1111

@@ -17,32 +17,34 @@ import kotlin.time.Duration
1717
* @param exceptionCausingViewCount The number of views that caused an exception during capture
1818
* @param viewCountAfterFilter The number of views after filtering
1919
* @param parseDuration The time it took to parse the view tree
20-
* @param captureTimeMs The total time it took to capture the screen (parse + encoding)
20+
* @param encodingTimeMs The time it took to encode all replay elements
2121
*/
22-
data class EncodedScreenMetrics(
22+
data class ReplayCaptureMetrics(
2323
var viewCount: Int = 0,
2424
var composeViewCount: Int = 0,
2525
var errorViewCount: Int = 0,
2626
var exceptionCausingViewCount: Int = 0,
2727
var viewCountAfterFilter: Int = 0,
2828
var parseDuration: Duration = Duration.ZERO,
29-
var captureTimeMs: Long = 0L,
29+
var encodingTimeMs: Long = 0L,
3030
) {
3131

32+
private val totalDurationMs: Long
33+
get() = parseDuration.inWholeMilliseconds + encodingTimeMs
34+
3235
/**
3336
* Convert the metrics to a map
3437
*/
3538
fun toMap(): Map<String, String> {
36-
/**
37-
* 'parseTime' is not included in the output map as it's passed to the Rust layer separately.
38-
*/
3939
return mapOf(
40-
"viewCount" to viewCount.toString(),
41-
"composeViewCount" to composeViewCount.toString(),
42-
"viewCountAfterFilter" to viewCountAfterFilter.toString(),
43-
"errorViewCount" to errorViewCount.toString(),
44-
"exceptionCausingViewCount" to exceptionCausingViewCount.toString(),
45-
"captureTimeMs" to captureTimeMs.toString(),
40+
"view_count" to viewCount.toString(),
41+
"compose_view_count" to composeViewCount.toString(),
42+
"view_count_after_filter" to viewCountAfterFilter.toString(),
43+
"error_view_count" to errorViewCount.toString(),
44+
"exception_causing_view_count" to exceptionCausingViewCount.toString(),
45+
"parse_duration_ms" to parseDuration.inWholeMilliseconds.toString(),
46+
"encoding_time_ms" to encodingTimeMs.toString(),
47+
"total_duration_ms" to totalDurationMs.toString(),
4648
)
4749
}
4850
}

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayPreviewClient.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import android.util.Log
1313
import io.bitdrift.capture.common.ErrorHandler
1414
import io.bitdrift.capture.common.MainThreadHandler
1515
import io.bitdrift.capture.replay.internal.DisplayManagers
16-
import io.bitdrift.capture.replay.internal.EncodedScreenMetrics
1716
import io.bitdrift.capture.replay.internal.FilteredCapture
1817
import io.bitdrift.capture.replay.internal.ReplayCaptureEngine
1918
import io.bitdrift.capture.replay.internal.WindowManager
@@ -25,7 +24,6 @@ import okhttp3.WebSocketListener
2524
import okio.ByteString
2625
import okio.ByteString.Companion.toByteString
2726
import java.util.concurrent.TimeUnit
28-
import kotlin.time.Duration
2927

3028
/**
3129
* Allows to capture the screen and send the binary data over a persistent websocket connection
@@ -95,7 +93,7 @@ class ReplayPreviewClient(
9593
}
9694
}
9795

98-
override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) {
96+
override fun onScreenCaptured(encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics) {
9997
lastEncodedScreen = encodedScreen
10098
webSocket?.send(encodedScreen.toByteString(0, encodedScreen.size))
10199
// forward the callback to the module's logger
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.bitdrift.capture.replay
2+
3+
data class ScreenshotCaptureMetrics(
4+
val screenshotTimeMs: Long,
5+
val screenshotAllocationByteCount: Int,
6+
val screenshotByteCount: Int,
7+
var compressionTimeMs: Long = 0,
8+
var compressionByteCount: Int = 0,
9+
) {
10+
val totalDurationMs: Long
11+
get() = screenshotTimeMs + compressionTimeMs
12+
13+
fun toMap(): Map<String, String> {
14+
return mapOf(
15+
"screenshot_time_ms" to screenshotTimeMs.toString(),
16+
"screenshot_allocation_byte_count" to screenshotAllocationByteCount.toString(),
17+
"screenshot_byte_count" to screenshotByteCount.toString(),
18+
"compression_time_ms" to compressionTimeMs.toString(),
19+
"compression_byte_count" to compressionByteCount.toString(),
20+
"total_duration_ms" to totalDurationMs.toString(),
21+
)
22+
}
23+
}

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ReplayCaptureController.kt renamed to platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/SessionReplayController.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.bitdrift.capture.common.ErrorHandler
1212
import io.bitdrift.capture.common.MainThreadHandler
1313
import io.bitdrift.capture.replay.internal.DisplayManagers
1414
import io.bitdrift.capture.replay.internal.ReplayCaptureEngine
15+
import io.bitdrift.capture.replay.internal.ScreenshotCaptureEngine
1516
import io.bitdrift.capture.replay.internal.WindowManager
1617

1718
/**
@@ -21,7 +22,7 @@ import io.bitdrift.capture.replay.internal.WindowManager
2122
* @param sessionReplayConfiguration the configuration to use
2223
* @param runtime allows for the feature to be remotely disabled
2324
*/
24-
class ReplayCaptureController(
25+
class SessionReplayController(
2526
errorHandler: ErrorHandler,
2627
replayLogger: IReplayLogger,
2728
screenshotLogger: IScreenshotLogger,

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayCaptureEngine.kt

+11-9
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import io.bitdrift.capture.common.DefaultClock
1111
import io.bitdrift.capture.common.ErrorHandler
1212
import io.bitdrift.capture.common.IClock
1313
import io.bitdrift.capture.common.MainThreadHandler
14-
import io.bitdrift.capture.replay.ReplayCaptureController
14+
import io.bitdrift.capture.replay.SessionReplayController
1515
import io.bitdrift.capture.replay.IReplayLogger
16+
import io.bitdrift.capture.replay.ReplayCaptureMetrics
1617
import io.bitdrift.capture.replay.SessionReplayConfiguration
1718
import java.util.concurrent.Executors
1819
import java.util.concurrent.ScheduledExecutorService
@@ -46,24 +47,25 @@ internal class ReplayCaptureEngine(
4647

4748
private fun captureScreen(
4849
skipReplayComposeViews: Boolean,
49-
completion: (encodedScreen: ByteArray, screen: FilteredCapture, metrics: EncodedScreenMetrics) -> Unit,
50+
completion: (encodedScreen: ByteArray, screen: FilteredCapture, metrics: ReplayCaptureMetrics) -> Unit,
5051
) {
5152
val startTime = clock.elapsedRealtime()
5253

53-
val encodedScreenMetrics = EncodedScreenMetrics()
54+
val replayCaptureMetrics = ReplayCaptureMetrics()
5455
val timedValue = measureTimedValue {
55-
captureParser.parse(encodedScreenMetrics, skipReplayComposeViews)
56+
captureParser.parse(replayCaptureMetrics, skipReplayComposeViews)
5657
}
5758

5859
executor.execute {
5960
captureFilter.filter(timedValue.value)?.let { filteredCapture ->
60-
encodedScreenMetrics.parseDuration = timedValue.duration
61-
encodedScreenMetrics.viewCountAfterFilter = filteredCapture.size
61+
replayCaptureMetrics.parseDuration = timedValue.duration
62+
replayCaptureMetrics.viewCountAfterFilter = filteredCapture.size
6263
val screen = captureDecorations.addDecorations(filteredCapture)
6364
val encodedScreen = replayEncoder.encode(screen)
64-
encodedScreenMetrics.captureTimeMs = clock.elapsedRealtime() - startTime
65-
ReplayCaptureController.L.d("Screen Captured: $encodedScreenMetrics")
66-
completion(encodedScreen, screen, encodedScreenMetrics)
65+
replayCaptureMetrics.encodingTimeMs =
66+
clock.elapsedRealtime() - startTime - replayCaptureMetrics.parseDuration.inWholeMilliseconds
67+
SessionReplayController.L.d("Screen Captured: $replayCaptureMetrics")
68+
completion(encodedScreen, screen, replayCaptureMetrics)
6769
}
6870
}
6971
}

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayDecorations.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ package io.bitdrift.capture.replay.internal
99

1010
import androidx.core.view.ViewCompat
1111
import androidx.core.view.WindowInsetsCompat
12-
import io.bitdrift.capture.common.ErrorHandler
13-
import io.bitdrift.capture.replay.ReplayCaptureController
12+
import io.bitdrift.capture.replay.SessionReplayController
1413
import io.bitdrift.capture.replay.ReplayType
1514

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

@@ -41,7 +40,7 @@ internal class ReplayDecorations(
4140
width = rootView.width,
4241
height = insets.bottom,
4342
)
44-
ReplayCaptureController.L.d("Keyboard IME size $imeBounds")
43+
SessionReplayController.L.d("Keyboard IME size $imeBounds")
4544
screen.add(imeBounds)
4645
}
4746
}

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ReplayParser.kt

+8-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
package io.bitdrift.capture.replay.internal
99

1010
import io.bitdrift.capture.common.ErrorHandler
11-
import io.bitdrift.capture.replay.ReplayCaptureController
11+
import io.bitdrift.capture.replay.SessionReplayController
12+
import io.bitdrift.capture.replay.ReplayCaptureMetrics
1213
import io.bitdrift.capture.replay.SessionReplayConfiguration
1314
import io.bitdrift.capture.replay.internal.mappers.ViewMapper
1415

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

3031
// Use a stack to perform a DFS traversal of the tree and avoid recursion
3132
val stack: ArrayDeque<ScannableView> = ArrayDeque(
3233
windowManager.findRootViews().map {
33-
ReplayCaptureController.L.v("Root view found and added to list: ${it.javaClass.simpleName}")
34+
SessionReplayController.L.v("Root view found and added to list: ${it.javaClass.simpleName}")
3435
ScannableView.AndroidView(it, skipReplayComposeViews)
3536
},
3637
)
3738
while (stack.isNotEmpty()) {
3839
val currentNode = stack.removeLast()
3940
try {
40-
viewMapper.updateMetrics(currentNode, encodedScreenMetrics)
41+
viewMapper.updateMetrics(currentNode, replayCaptureMetrics)
4142
if (!viewMapper.viewIsVisible(currentNode)) {
42-
ReplayCaptureController.L.v("Ignoring not visible view: ${currentNode.displayName}")
43+
SessionReplayController.L.v("Ignoring not visible view: ${currentNode.displayName}")
4344
continue
4445
}
4546
result.add(viewMapper.mapView(currentNode))
4647
} catch (e: Throwable) {
4748
val errorMsg = "Error parsing view, Skipping $currentNode and children"
48-
ReplayCaptureController.L.e(e, errorMsg)
49-
encodedScreenMetrics.exceptionCausingViewCount += 1
49+
SessionReplayController.L.e(e, errorMsg)
50+
replayCaptureMetrics.exceptionCausingViewCount += 1
5051
errorHandler.handleError(errorMsg, e)
5152
}
5253
// Convert the sequence of children to a list to process in reverse order

platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/ScreenshotCaptureEngine.kt renamed to platform/jvm/replay/src/main/kotlin/io/bitdrift/capture/replay/internal/ScreenshotCaptureEngine.kt

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.bitdrift.capture.replay
1+
package io.bitdrift.capture.replay.internal
22

33
import android.content.Context
44
import android.graphics.Bitmap
@@ -9,8 +9,8 @@ import io.bitdrift.capture.common.DefaultClock
99
import io.bitdrift.capture.common.ErrorHandler
1010
import io.bitdrift.capture.common.IClock
1111
import io.bitdrift.capture.common.MainThreadHandler
12-
import io.bitdrift.capture.replay.internal.DisplayManagers
13-
import io.bitdrift.capture.replay.internal.WindowManager
12+
import io.bitdrift.capture.replay.IScreenshotLogger
13+
import io.bitdrift.capture.replay.ScreenshotCaptureMetrics
1414
import java.io.ByteArrayOutputStream
1515
import java.util.concurrent.Executors
1616
import java.util.concurrent.ScheduledExecutorService
@@ -38,9 +38,10 @@ internal class ScreenshotCaptureEngine(
3838
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
3939
return
4040
}
41-
val startTime = clock.elapsedRealtime()
41+
val startTimeMs = clock.elapsedRealtime()
42+
// TODO(murki): Log empty screenshot on unblock the caller if there are no root views
4243
val topView = windowManager.findRootViews().lastOrNull() ?: return
43-
// TODO(murki): Consider calling setDestinationBitmap() with a Bitmap.Config.RGB_565 instead
44+
// TODO(murki): Reduce memory footprint by calling setDestinationBitmap() with a Bitmap.Config.RGB_565 instead
4445
// of the default of Bitmap.Config.ARGB_8888
4546
val screenshotRequest = PixelCopy.Request.Builder.ofWindow(topView).build()
4647
PixelCopy.request(screenshotRequest, executor) { screenshotResult ->
@@ -50,28 +51,26 @@ internal class ScreenshotCaptureEngine(
5051
return@request
5152
}
5253

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

7776
}

0 commit comments

Comments
 (0)