Skip to content

Commit 24bdc2c

Browse files
authored
Merge pull request #2513 from DataDog/tvaleev/RUM-8785
[RUM-8785]: Refactoring JankStatsActivityLifecycleListener
2 parents 3238970 + a03d8e8 commit 24bdc2c

File tree

13 files changed

+471
-170
lines changed

13 files changed

+471
-170
lines changed

detekt_custom.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ datadog:
424424
- "android.view.View.getChildAt(kotlin.Int)"
425425
- "android.view.View.getTag(kotlin.Int)"
426426
- "android.view.View.hashCode()"
427+
- "android.view.View.post(java.lang.Runnable?)"
427428
- "android.view.View.setTag(kotlin.Int, kotlin.Any?)"
428429
- "android.view.ViewGroup.findViewById(kotlin.Int)"
429430
- "android.view.ViewGroup.getChildAt(kotlin.Int)"
@@ -1097,6 +1098,7 @@ datadog:
10971098
- "kotlin.collections.emptySet()"
10981099
- "kotlin.collections.listOf(android.view.Window)"
10991100
- "kotlin.collections.listOf(com.datadog.android.api.InternalLogger.Target)"
1101+
- "kotlin.collections.listOf(com.datadog.android.rum.internal.vitals.FPSVitalListener)"
11001102
- "kotlin.collections.listOf(com.datadog.android.rum.model.ActionEvent.Interface)"
11011103
- "kotlin.collections.listOf(com.datadog.android.rum.model.ErrorEvent.Interface)"
11021104
- "kotlin.collections.listOf(com.datadog.android.rum.model.LongTaskEvent.Interface)"

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import com.datadog.android.rum.internal.tracking.NoOpUserActionTrackingStrategy
6060
import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy
6161
import com.datadog.android.rum.internal.vitals.AggregatingVitalMonitor
6262
import com.datadog.android.rum.internal.vitals.CPUVitalReader
63+
import com.datadog.android.rum.internal.vitals.FPSVitalListener
6364
import com.datadog.android.rum.internal.vitals.JankStatsActivityLifecycleListener
6465
import com.datadog.android.rum.internal.vitals.MemoryVitalReader
6566
import com.datadog.android.rum.internal.vitals.NoOpVitalMonitor
@@ -416,7 +417,9 @@ internal class RumFeature(
416417
)
417418

418419
jankStatsActivityLifecycleListener = JankStatsActivityLifecycleListener(
419-
frameRateVitalMonitor,
420+
listOf(
421+
FPSVitalListener(frameRateVitalMonitor)
422+
),
420423
sdkCore.internalLogger
421424
)
422425
(appContext as? Application)?.registerActivityLifecycleCallbacks(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
package com.datadog.android.rum.internal.domain
7+
8+
import android.os.Build
9+
import androidx.annotation.RequiresApi
10+
11+
internal data class FrameMetricsData(
12+
@RequiresApi(Build.VERSION_CODES.N) var unknownDelayDuration: Long = 0L,
13+
@RequiresApi(Build.VERSION_CODES.N) var inputHandlingDuration: Long = 0L,
14+
@RequiresApi(Build.VERSION_CODES.N) var animationDuration: Long = 0L,
15+
@RequiresApi(Build.VERSION_CODES.N) var layoutMeasureDuration: Long = 0L,
16+
@RequiresApi(Build.VERSION_CODES.N) var drawDuration: Long = 0L,
17+
@RequiresApi(Build.VERSION_CODES.N) var syncDuration: Long = 0L,
18+
@RequiresApi(Build.VERSION_CODES.N) var commandIssueDuration: Long = 0L,
19+
@RequiresApi(Build.VERSION_CODES.N) var swapBuffersDuration: Long = 0L,
20+
@RequiresApi(Build.VERSION_CODES.N) var totalDuration: Long = 0L,
21+
@RequiresApi(Build.VERSION_CODES.N) var firstDrawFrame: Boolean = false,
22+
@RequiresApi(Build.VERSION_CODES.O) var intendedVsyncTimestamp: Long = 0L,
23+
@RequiresApi(Build.VERSION_CODES.O) var vsyncTimestamp: Long = 0L,
24+
@RequiresApi(Build.VERSION_CODES.S) var gpuDuration: Long = 0L,
25+
@RequiresApi(Build.VERSION_CODES.S) var deadline: Long = 0L,
26+
var displayRefreshRate: Double = SIXTY_FPS
27+
) {
28+
companion object {
29+
private const val SIXTY_FPS: Double = 60.0
30+
}
31+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
package com.datadog.android.rum.internal.vitals
7+
8+
import android.annotation.SuppressLint
9+
import android.os.Build
10+
import androidx.annotation.RequiresApi
11+
import androidx.metrics.performance.FrameData
12+
import com.datadog.android.core.internal.system.BuildSdkVersionProvider
13+
import com.datadog.android.rum.internal.domain.FrameMetricsData
14+
import java.util.concurrent.TimeUnit
15+
16+
internal class FPSVitalListener(
17+
private val vitalObserver: VitalObserver,
18+
private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT,
19+
private var screenRefreshRate: Double = 60.0
20+
) : FrameStateListener {
21+
@RequiresApi(Build.VERSION_CODES.S)
22+
private var frameDeadline = EXPECTED_60_FPS_FRAME_DURATION_NS
23+
private var displayRefreshRate: Double = SIXTY_FPS
24+
25+
override fun onFrame(volatileFrameData: FrameData) {
26+
val durationNs = volatileFrameData.frameDurationUiNanos
27+
if (durationNs > 0.0) {
28+
var frameRate = (ONE_SECOND_NS / durationNs)
29+
30+
@SuppressLint("NewApi")
31+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
32+
screenRefreshRate = ONE_SECOND_NS / frameDeadline
33+
} else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
34+
screenRefreshRate = displayRefreshRate
35+
}
36+
37+
// If normalized frame rate is still at over 60fps it means the frame rendered
38+
// quickly enough for the devices refresh rate.
39+
frameRate = (frameRate * (SIXTY_FPS / screenRefreshRate)).coerceAtMost(MAX_FPS)
40+
41+
if (frameRate > MIN_FPS) {
42+
vitalObserver.onNewSample(frameRate)
43+
}
44+
}
45+
}
46+
47+
override fun onFrameMetricsData(data: FrameMetricsData) {
48+
displayRefreshRate = data.displayRefreshRate
49+
@SuppressLint("NewApi")
50+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
51+
frameDeadline = data.deadline
52+
}
53+
}
54+
55+
companion object {
56+
private const val EXPECTED_60_FPS_FRAME_DURATION_NS: Long = 16_666_666L
57+
private val ONE_SECOND_NS: Double = TimeUnit.SECONDS.toNanos(1).toDouble()
58+
59+
private const val MIN_FPS: Double = 1.0
60+
private const val MAX_FPS: Double = 60.0
61+
private const val SIXTY_FPS: Double = 60.0
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
package com.datadog.android.rum.internal.vitals
7+
8+
import com.datadog.android.rum.internal.domain.FrameMetricsData
9+
10+
internal interface FrameMetricsDataListener {
11+
fun onFrameMetricsData(data: FrameMetricsData)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
package com.datadog.android.rum.internal.vitals
7+
8+
import androidx.metrics.performance.JankStats
9+
10+
internal interface FrameStateListener : JankStats.OnFrameListener, FrameMetricsDataListener

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/vitals/JankStatsActivityLifecycleListener.kt

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,17 @@ import androidx.metrics.performance.FrameData
2424
import androidx.metrics.performance.JankStats
2525
import com.datadog.android.api.InternalLogger
2626
import com.datadog.android.core.internal.system.BuildSdkVersionProvider
27+
import com.datadog.android.rum.internal.domain.FrameMetricsData
2728
import java.lang.ref.WeakReference
2829
import java.util.WeakHashMap
29-
import java.util.concurrent.TimeUnit
3030

3131
/**
3232
* Utility class listening to frame rate information.
3333
*/
3434
internal class JankStatsActivityLifecycleListener(
35-
private val vitalObserver: VitalObserver,
35+
private val delegates: List<FrameStateListener>,
3636
private val internalLogger: InternalLogger,
3737
private val jankStatsProvider: JankStatsProvider = JankStatsProvider.DEFAULT,
38-
private var screenRefreshRate: Double = 60.0,
3938
private var buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT
4039
) : ActivityLifecycleCallbacks, JankStats.OnFrameListener {
4140

@@ -44,7 +43,8 @@ internal class JankStatsActivityLifecycleListener(
4443
internal val activeActivities = WeakHashMap<Window, MutableList<WeakReference<Activity>>>()
4544
internal var display: Display? = null
4645
private var frameMetricsListener: DDFrameMetricsListener? = null
47-
internal var frameDeadline = SIXTEEN_MS_NS
46+
47+
private val frameMetricsData = FrameMetricsData()
4848

4949
// region ActivityLifecycleCallbacks
5050
@MainThread
@@ -143,7 +143,7 @@ internal class JankStatsActivityLifecycleListener(
143143
if (activeActivities[activity.window].isNullOrEmpty()) {
144144
activeWindowsListener.remove(activity.window)
145145
activeActivities.remove(activity.window)
146-
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
146+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N) {
147147
unregisterMetricListener(activity.window)
148148
}
149149
}
@@ -154,23 +154,8 @@ internal class JankStatsActivityLifecycleListener(
154154
// region JankStats.OnFrameListener
155155

156156
override fun onFrame(volatileFrameData: FrameData) {
157-
val durationNs = volatileFrameData.frameDurationUiNanos
158-
if (durationNs > 0.0) {
159-
var frameRate = (ONE_SECOND_NS / durationNs)
160-
161-
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
162-
screenRefreshRate = ONE_SECOND_NS / frameDeadline
163-
} else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
164-
screenRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS
165-
}
166-
167-
// If normalized frame rate is still at over 60fps it means the frame rendered
168-
// quickly enough for the devices refresh rate.
169-
frameRate = (frameRate * (SIXTY_FPS / screenRefreshRate)).coerceAtMost(MAX_FPS)
170-
171-
if (frameRate > MIN_FPS) {
172-
vitalObserver.onNewSample(frameRate)
173-
}
157+
for (i in delegates.indices) {
158+
delegates[i].onFrame(volatileFrameData)
174159
}
175160
}
176161

@@ -216,7 +201,7 @@ internal class JankStatsActivityLifecycleListener(
216201
@SuppressLint("NewApi")
217202
@MainThread
218203
private fun trackWindowMetrics(isKnownWindow: Boolean, window: Window, activity: Activity) {
219-
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S && !isKnownWindow) {
204+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N && !isKnownWindow) {
220205
registerMetricListener(window)
221206
} else if (display == null && buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
222207
// Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have
@@ -226,14 +211,37 @@ internal class JankStatsActivityLifecycleListener(
226211
}
227212
}
228213

229-
@RequiresApi(Build.VERSION_CODES.S)
214+
@RequiresApi(Build.VERSION_CODES.N)
230215
private fun registerMetricListener(window: Window) {
231216
if (frameMetricsListener == null) {
232217
frameMetricsListener = DDFrameMetricsListener()
233218
}
219+
// TODO RUM-8799: handler thread can be used instead
234220
val handler = Handler(Looper.getMainLooper())
235-
// Only hardware accelerated views can be tracked with metrics listener
236-
if (window.peekDecorView()?.isHardwareAccelerated == true) {
221+
val decorView = window.peekDecorView()
222+
223+
if (decorView == null) {
224+
internalLogger.log(
225+
InternalLogger.Level.WARN,
226+
InternalLogger.Target.MAINTAINER,
227+
{ "Unable to attach JankStatsListener to window, decorView is null" }
228+
)
229+
return
230+
}
231+
232+
// We need to postpone this operation because isHardwareAccelerated will return
233+
// false until the view is attached to the window. Note that in this case main looper should be used
234+
decorView.post {
235+
// Only hardware accelerated views can be tracked with metrics listener
236+
if (!decorView.isHardwareAccelerated) {
237+
internalLogger.log(
238+
InternalLogger.Level.WARN,
239+
InternalLogger.Target.MAINTAINER,
240+
{ "Unable to attach JankStatsListener to window, decorView is not hardware accelerated" }
241+
)
242+
return@post
243+
}
244+
237245
frameMetricsListener?.let { listener ->
238246
try {
239247
@Suppress("UnsafeThirdPartyFunctionCall") // Listener can't be null here
@@ -247,12 +255,6 @@ internal class JankStatsActivityLifecycleListener(
247255
)
248256
}
249257
}
250-
} else {
251-
internalLogger.log(
252-
InternalLogger.Level.WARN,
253-
InternalLogger.Target.MAINTAINER,
254-
{ "Unable to attach JankStatsListener to window, decorView is null or not hardware accelerated" }
255-
)
256258
}
257259
}
258260

@@ -273,13 +275,42 @@ internal class JankStatsActivityLifecycleListener(
273275
@RequiresApi(Build.VERSION_CODES.N)
274276
inner class DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener {
275277

276-
@RequiresApi(Build.VERSION_CODES.S)
278+
@RequiresApi(Build.VERSION_CODES.N)
277279
override fun onFrameMetricsAvailable(
278280
window: Window,
279281
frameMetrics: FrameMetrics,
280282
dropCountSinceLastInvocation: Int
281283
) {
282-
frameDeadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
284+
for (i in delegates.indices) {
285+
delegates[i].onFrameMetricsData(frameMetricsData.update(frameMetrics))
286+
}
287+
}
288+
}
289+
290+
@RequiresApi(Build.VERSION_CODES.N)
291+
private fun FrameMetricsData.update(frameMetrics: FrameMetrics) = apply {
292+
displayRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS
293+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N) {
294+
unknownDelayDuration = frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)
295+
inputHandlingDuration = frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)
296+
animationDuration = frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)
297+
layoutMeasureDuration = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)
298+
drawDuration = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)
299+
syncDuration = frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
300+
commandIssueDuration = frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION)
301+
swapBuffersDuration = frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
302+
totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
303+
firstDrawFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == IS_FIRST_DRAW_FRAME
304+
}
305+
@SuppressLint("InlinedApi")
306+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.O) {
307+
intendedVsyncTimestamp = frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
308+
vsyncTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
309+
}
310+
@SuppressLint("InlinedApi")
311+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
312+
gpuDuration = frameMetrics.getMetric(FrameMetrics.GPU_DURATION)
313+
deadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
283314
}
284315
}
285316

@@ -292,12 +323,7 @@ internal class JankStatsActivityLifecycleListener(
292323
" shouldn't happen."
293324
internal const val JANK_STATS_TRACKING_DISABLE_ERROR =
294325
"Failed to disable JankStats tracking"
295-
296-
private val ONE_SECOND_NS: Double = TimeUnit.SECONDS.toNanos(1).toDouble()
297-
298-
private const val MIN_FPS: Double = 1.0
299-
private const val MAX_FPS: Double = 60.0
300326
private const val SIXTY_FPS: Double = 60.0
301-
private const val SIXTEEN_MS_NS: Long = 16666666
327+
private const val IS_FIRST_DRAW_FRAME = 1L
302328
}
303329
}

0 commit comments

Comments
 (0)