Skip to content

Commit a6f7ed2

Browse files
authored
Merge pull request #2052 from DataDog/xgouchet/RUM-4561/sr_drawable_perf
RUM-4561 Cherry pick Drawable Performance improvements
2 parents 45b2914 + 3122210 commit a6f7ed2

File tree

7 files changed

+58
-35
lines changed

7 files changed

+58
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [IMPROVEMENT] Session Replay: Add telemetry to detect uncovered View/Drawable in Session Replay. See [#2028](https://github.com/DataDog/dd-sdk-android/pull/2028)
1010
* [IMPROVEMENT] Session Replay: Improve `SeekBarMapper`. See [#2037](https://github.com/DataDog/dd-sdk-android/pull/2037)
1111
* [IMPROVEMENT] RUM: Flag critical events in custom persistence. See [#2044](https://github.com/DataDog/dd-sdk-android/pull/2044)
12+
* [IMPROVEMENT] Delegate Drawable copy to background thread. See [#2048](https://github.com/DataDog/dd-sdk-android/pull/2048)
1213
* [MAINTENANCE] Next dev iteration. See [#2020](https://github.com/DataDog/dd-sdk-android/pull/2020)
1314
* [MAINTENANCE] Merge release `2.9.0` into `develop` branch. See [#2023](https://github.com/DataDog/dd-sdk-android/pull/2023)
1415
* [MAINTENANCE] Session Replay: Improve UT for SR Obfuscators. See [#2031](https://github.com/DataDog/dd-sdk-android/pull/2031)

features/dd-sdk-android-session-replay/consumer-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
# Kept for our internal telemetry
77
-keepnames class com.datadog.android.sessionreplay.internal.recorder.listener.WindowsOnDrawListener
8+
-keepnames class * extends com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
130130
applicationId = applicationId,
131131
recordedDataQueueHandler = recordedDataQueueHandler,
132132
bitmapCachesManager = bitmapCachesManager,
133-
drawableUtils = DrawableUtils(internalLogger, bitmapCachesManager),
133+
drawableUtils = DrawableUtils(
134+
internalLogger,
135+
bitmapCachesManager,
136+
sdkCore.createSingleThreadExecutorService("drawables")
137+
),
134138
logger = internalLogger,
135139
md5HashGenerator = MD5HashGenerator(internalLogger),
136140
webPImageCompression = WebPImageCompression(internalLogger)

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ package com.datadog.android.sessionreplay.internal.recorder
99
import android.view.View
1010
import android.view.ViewGroup
1111
import com.datadog.android.api.InternalLogger
12+
import com.datadog.android.api.feature.measureMethodCallPerf
1213
import com.datadog.android.sessionreplay.MapperTypeWrapper
1314
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
1415
import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueStatusCallback
1516
import com.datadog.android.sessionreplay.model.MobileSegment
1617
import com.datadog.android.sessionreplay.recorder.MappingContext
1718
import com.datadog.android.sessionreplay.recorder.mapper.TraverseAllChildrenMapper
1819
import com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper
20+
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
1921
import com.datadog.android.sessionreplay.utils.NoOpAsyncJobStatusCallback
2022

2123
internal class TreeViewTraversal(
@@ -39,29 +41,32 @@ internal class TreeViewTraversal(
3941
}
4042

4143
val traversalStrategy: TraversalStrategy
42-
val resolvedWireframes: List<MobileSegment.Wireframe>
44+
4345
val noOpCallback = NoOpAsyncJobStatusCallback()
46+
val jobStatusCallback: AsyncJobStatusCallback
4447

4548
// try to resolve from the exhaustive type mappers
46-
val mapper = findMapperForView(view)
49+
var mapper = findMapperForView(view)
4750

4851
if (mapper != null) {
49-
val queueStatusCallback = QueueStatusCallback(recordedDataQueueRefs)
52+
jobStatusCallback = QueueStatusCallback(recordedDataQueueRefs)
5053
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
5154
TraversalStrategy.TRAVERSE_ALL_CHILDREN
5255
} else {
5356
TraversalStrategy.STOP_AND_RETURN_NODE
5457
}
55-
resolvedWireframes = mapper.map(view, mappingContext, queueStatusCallback, internalLogger)
5658
} else if (isDecorView(view)) {
5759
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
58-
resolvedWireframes = decorViewMapper.map(view, mappingContext, noOpCallback, internalLogger)
60+
mapper = decorViewMapper
61+
jobStatusCallback = noOpCallback
5962
} else if (view is ViewGroup) {
6063
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
61-
resolvedWireframes = defaultViewMapper.map(view, mappingContext, noOpCallback, internalLogger)
64+
mapper = defaultViewMapper
65+
jobStatusCallback = noOpCallback
6266
} else {
6367
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
64-
resolvedWireframes = defaultViewMapper.map(view, mappingContext, noOpCallback, internalLogger)
68+
mapper = defaultViewMapper
69+
jobStatusCallback = noOpCallback
6570
val viewType = view.javaClass.canonicalName ?: view.javaClass.name
6671

6772
internalLogger.log(
@@ -76,6 +81,14 @@ internal class TreeViewTraversal(
7681
)
7782
}
7883

84+
val resolvedWireframes = internalLogger.measureMethodCallPerf(
85+
javaClass,
86+
"$METHOD_CALL_MAP_PREFIX ${mapper.javaClass.simpleName}",
87+
METHOD_CALL_SAMPLING_RATE
88+
) {
89+
mapper.map(view, mappingContext, jobStatusCallback, internalLogger)
90+
}
91+
7992
return TraversedTreeView(resolvedWireframes, traversalStrategy)
8093
}
8194

@@ -93,4 +106,9 @@ internal class TreeViewTraversal(
93106
val mappedWireframes: List<MobileSegment.Wireframe>,
94107
val nextActionStrategy: TraversalStrategy
95108
)
109+
110+
companion object {
111+
const val METHOD_CALL_SAMPLING_RATE = 5f
112+
private const val METHOD_CALL_MAP_PREFIX: String = "Map with"
113+
}
96114
}

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/*
32
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
43
* This product includes software developed at Datadog (https://www.datadoghq.com/).
@@ -13,25 +12,25 @@ import android.graphics.Bitmap.Config
1312
import android.graphics.Color
1413
import android.graphics.PorterDuff
1514
import android.graphics.drawable.Drawable
16-
import android.os.Handler
17-
import android.os.Looper
1815
import android.util.DisplayMetrics
1916
import androidx.annotation.MainThread
2017
import androidx.annotation.VisibleForTesting
2118
import androidx.annotation.WorkerThread
2219
import com.datadog.android.api.InternalLogger
20+
import com.datadog.android.core.internal.utils.submitSafe
2321
import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapCachesManager
2422
import com.datadog.android.sessionreplay.internal.recorder.resources.ResourceResolver
2523
import com.datadog.android.sessionreplay.internal.recorder.wrappers.BitmapWrapper
2624
import com.datadog.android.sessionreplay.internal.recorder.wrappers.CanvasWrapper
25+
import java.util.concurrent.ExecutorService
2726
import kotlin.math.sqrt
2827

2928
internal class DrawableUtils(
30-
private val logger: InternalLogger,
29+
private val internalLogger: InternalLogger,
3130
private val bitmapCachesManager: BitmapCachesManager,
32-
private val bitmapWrapper: BitmapWrapper = BitmapWrapper(logger),
33-
private val canvasWrapper: CanvasWrapper = CanvasWrapper(logger),
34-
private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
31+
private val executorService: ExecutorService,
32+
private val bitmapWrapper: BitmapWrapper = BitmapWrapper(internalLogger),
33+
private val canvasWrapper: CanvasWrapper = CanvasWrapper(internalLogger)
3534
) {
3635

3736
/**
@@ -59,8 +58,8 @@ internal class DrawableUtils(
5958
resizeBitmapCallback = object :
6059
ResizeBitmapCallback {
6160
override fun onSuccess(bitmap: Bitmap) {
62-
mainThreadHandler.post {
63-
@Suppress("ThreadSafety") // this runs on the main thread
61+
executorService.submitSafe("drawOnCanvas", internalLogger) {
62+
@Suppress("ThreadSafety") // this runs on a background thread
6463
drawOnCanvas(
6564
resources,
6665
bitmap,
@@ -71,7 +70,7 @@ internal class DrawableUtils(
7170
}
7271

7372
override fun onFailure() {
74-
logger.log(
73+
internalLogger.log(
7574
InternalLogger.Level.ERROR,
7675
InternalLogger.Target.MAINTAINER,
7776
{ FAILED_TO_CREATE_SCALED_BITMAP_ERROR }

features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.Extensions
3636
import org.mockito.Mock
3737
import org.mockito.junit.jupiter.MockitoExtension
3838
import org.mockito.junit.jupiter.MockitoSettings
39+
import org.mockito.kotlin.any
3940
import org.mockito.kotlin.argumentCaptor
4041
import org.mockito.kotlin.doReturn
4142
import org.mockito.kotlin.eq
@@ -50,6 +51,7 @@ import org.mockito.quality.Strictness
5051
import java.util.Locale
5152
import java.util.UUID
5253
import java.util.concurrent.CountDownLatch
54+
import java.util.concurrent.ExecutorService
5355
import java.util.concurrent.TimeUnit
5456

5557
@Extensions(
@@ -75,6 +77,9 @@ internal class SessionReplayFeatureTest {
7577
@Mock
7678
lateinit var mockInternalLogger: InternalLogger
7779

80+
@Mock
81+
lateinit var mockExecutorService: ExecutorService
82+
7883
@Mock
7984
lateinit var mockSampler: Sampler
8085

@@ -88,6 +93,8 @@ internal class SessionReplayFeatureTest {
8893
whenever(mockSampler.getSampleRate()).thenReturn(fakeSampleRate)
8994
fakeSessionId = UUID.randomUUID().toString()
9095
whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger
96+
whenever(mockSdkCore.createSingleThreadExecutorService(any())) doReturn mockExecutorService
97+
9198
testedFeature = SessionReplayFeature(
9299
sdkCore = mockSdkCore,
93100
customEndpointUrl = fakeConfiguration.customEndpointUrl,

features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import android.graphics.Bitmap
1111
import android.graphics.Canvas
1212
import android.graphics.drawable.Drawable
1313
import android.graphics.drawable.Drawable.ConstantState
14-
import android.os.Handler
1514
import android.util.DisplayMetrics
1615
import com.datadog.android.api.InternalLogger
1716
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
@@ -48,6 +47,7 @@ import java.util.concurrent.Future
4847
@MockitoSettings(strictness = Strictness.LENIENT)
4948
@ForgeConfiguration(ForgeConfigurator::class)
5049
internal class DrawableUtilsTest {
50+
5151
private lateinit var testedDrawableUtils: DrawableUtils
5252

5353
@Mock
@@ -80,9 +80,6 @@ internal class DrawableUtilsTest {
8080
@Mock
8181
private lateinit var mockBitmapCreationCallback: ResourceResolver.BitmapCreationCallback
8282

83-
@Mock
84-
private lateinit var mockMainThreadHandler: Handler
85-
8683
@Mock
8784
lateinit var mockConstantState: ConstantState
8885

@@ -95,6 +92,9 @@ internal class DrawableUtilsTest {
9592
@Mock
9693
private lateinit var mockLogger: InternalLogger
9794

95+
@Mock
96+
private lateinit var mockFuture: Future<Unit>
97+
9898
@BeforeEach
9999
fun setup() {
100100
whenever(mockConstantState.newDrawable(mockResources)).thenReturn(mockSecondDrawable)
@@ -106,25 +106,18 @@ internal class DrawableUtilsTest {
106106
whenever(mockBitmap.config).thenReturn(mockConfig)
107107
whenever(mockBitmapCachesManager.getBitmapByProperties(any(), any(), any())).thenReturn(null)
108108

109-
doAnswer { invocation ->
110-
val work = invocation.getArgument(0) as Runnable
111-
work.run()
112-
null
113-
}.whenever(mockMainThreadHandler).post(
114-
any()
115-
)
116-
117-
whenever(mockExecutorService.execute(any())).then {
118-
(it.arguments[0] as Runnable).run()
119-
mock<Future<Boolean>>()
109+
whenever(mockExecutorService.submit(any())) doAnswer {
110+
val runnable = it.getArgument<Runnable>(0)
111+
runnable.run()
112+
mockFuture
120113
}
121114

122115
testedDrawableUtils = DrawableUtils(
123116
bitmapWrapper = mockBitmapWrapper,
124117
canvasWrapper = mockCanvasWrapper,
125118
bitmapCachesManager = mockBitmapCachesManager,
126-
mainThreadHandler = mockMainThreadHandler,
127-
logger = mockLogger
119+
executorService = mockExecutorService,
120+
internalLogger = mockLogger
128121
)
129122
}
130123

0 commit comments

Comments
 (0)