Skip to content

Commit be6f712

Browse files
committed
RUM-8785: refactoring current jank stats logic - decoupling from FPS computing
1 parent c0044da commit be6f712

File tree

12 files changed

+430
-176
lines changed

12 files changed

+430
-176
lines changed

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

Lines changed: 5 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,10 @@ internal class RumFeature(
416417
)
417418

418419
jankStatsActivityLifecycleListener = JankStatsActivityLifecycleListener(
419-
frameRateVitalMonitor,
420+
@Suppress("UnsafeThirdPartyFunctionCall") // it's safe to call listOf here
421+
listOf(
422+
FPSVitalListener(frameRateVitalMonitor)
423+
),
420424
sdkCore.internalLogger
421425
)
422426
(appContext as? Application)?.registerActivityLifecycleCallbacks(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 = 1.0
27+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.os.Build
9+
import androidx.metrics.performance.FrameData
10+
import com.datadog.android.core.internal.system.BuildSdkVersionProvider
11+
import com.datadog.android.rum.internal.domain.FrameMetricsData
12+
import java.util.concurrent.TimeUnit
13+
14+
internal class FPSVitalListener(
15+
private val vitalObserver: VitalObserver,
16+
private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT,
17+
private var screenRefreshRate: Double = 60.0
18+
) : FrameStateListener {
19+
private var frameDeadline = SIXTEEN_MS_NS
20+
private var displayRefreshRate: Double = SIXTY_FPS
21+
22+
override fun onFrame(volatileFrameData: FrameData) {
23+
val durationNs = volatileFrameData.frameDurationUiNanos
24+
if (durationNs > 0.0) {
25+
var frameRate = (ONE_SECOND_NS / durationNs)
26+
27+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
28+
screenRefreshRate = ONE_SECOND_NS / frameDeadline
29+
} else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
30+
screenRefreshRate = displayRefreshRate
31+
}
32+
33+
// If normalized frame rate is still at over 60fps it means the frame rendered
34+
// quickly enough for the devices refresh rate.
35+
frameRate = (frameRate * (SIXTY_FPS / screenRefreshRate)).coerceAtMost(MAX_FPS)
36+
37+
if (frameRate > MIN_FPS) {
38+
vitalObserver.onNewSample(frameRate)
39+
}
40+
}
41+
}
42+
43+
override fun onFrameMetricsData(data: FrameMetricsData) {
44+
displayRefreshRate = data.displayRefreshRate
45+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
46+
frameDeadline = data.deadline
47+
}
48+
}
49+
50+
companion object {
51+
private const val SIXTEEN_MS_NS: Long = 16666666
52+
private val ONE_SECOND_NS: Double = TimeUnit.SECONDS.toNanos(1).toDouble()
53+
54+
private const val MIN_FPS: Double = 1.0
55+
private const val MAX_FPS: Double = 60.0
56+
private const val SIXTY_FPS: Double = 60.0
57+
}
58+
}
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: 45 additions & 49 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
@@ -154,24 +154,7 @@ 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-
}
174-
}
157+
delegates.forEach { it.onFrame(volatileFrameData) }
175158
}
176159

177160
// endregion
@@ -233,30 +216,22 @@ internal class JankStatsActivityLifecycleListener(
233216
}
234217
val handler = Handler(Looper.getMainLooper())
235218
// Only hardware accelerated views can be tracked with metrics listener
236-
if (window.peekDecorView()?.isHardwareAccelerated == true) {
237-
frameMetricsListener?.let { listener ->
238-
try {
239-
@Suppress("UnsafeThirdPartyFunctionCall") // Listener can't be null here
240-
window.addOnFrameMetricsAvailableListener(listener, handler)
241-
} catch (e: IllegalStateException) {
242-
internalLogger.log(
243-
InternalLogger.Level.ERROR,
244-
InternalLogger.Target.MAINTAINER,
245-
{ "Unable to attach JankStatsListener to window" },
246-
e
247-
)
248-
}
219+
frameMetricsListener?.let { listener ->
220+
try {
221+
@Suppress("UnsafeThirdPartyFunctionCall") // Listener can't be null here
222+
window.addOnFrameMetricsAvailableListener(listener, handler)
223+
} catch (e: IllegalStateException) {
224+
internalLogger.log(
225+
InternalLogger.Level.ERROR,
226+
InternalLogger.Target.MAINTAINER,
227+
{ "Unable to attach JankStatsListener to window" },
228+
e
229+
)
249230
}
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-
)
256231
}
257232
}
258233

259-
@RequiresApi(Build.VERSION_CODES.N)
234+
@RequiresApi(Build.VERSION_CODES.S)
260235
private fun unregisterMetricListener(window: Window) {
261236
try {
262237
window.removeOnFrameMetricsAvailableListener(frameMetricsListener)
@@ -279,7 +254,34 @@ internal class JankStatsActivityLifecycleListener(
279254
frameMetrics: FrameMetrics,
280255
dropCountSinceLastInvocation: Int
281256
) {
282-
frameDeadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
257+
delegates.forEach { it.onFrameMetricsData(frameMetricsData.update(frameMetrics)) }
258+
}
259+
}
260+
261+
@RequiresApi(Build.VERSION_CODES.N)
262+
private fun FrameMetricsData.update(frameMetrics: FrameMetrics) = apply {
263+
displayRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS
264+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N) {
265+
unknownDelayDuration = frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)
266+
inputHandlingDuration = frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)
267+
animationDuration = frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)
268+
layoutMeasureDuration = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)
269+
drawDuration = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)
270+
syncDuration = frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
271+
commandIssueDuration = frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION)
272+
swapBuffersDuration = frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
273+
totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
274+
firstDrawFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1L
275+
}
276+
@SuppressLint("InlinedApi")
277+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.O) {
278+
intendedVsyncTimestamp = frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
279+
vsyncTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
280+
}
281+
@SuppressLint("InlinedApi")
282+
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
283+
gpuDuration = frameMetrics.getMetric(FrameMetrics.GPU_DURATION)
284+
deadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
283285
}
284286
}
285287

@@ -292,12 +294,6 @@ internal class JankStatsActivityLifecycleListener(
292294
" shouldn't happen."
293295
internal const val JANK_STATS_TRACKING_DISABLE_ERROR =
294296
"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
300297
private const val SIXTY_FPS: Double = 60.0
301-
private const val SIXTEEN_MS_NS: Long = 16666666
302298
}
303299
}

0 commit comments

Comments
 (0)