Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RUM-8785]: Refactoring JankStatsActivityLifecycleListener #2513

Merged
merged 4 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ datadog:
- "android.view.View.getChildAt(kotlin.Int)"
- "android.view.View.getTag(kotlin.Int)"
- "android.view.View.hashCode()"
- "android.view.View.post(java.lang.Runnable?)"
- "android.view.View.setTag(kotlin.Int, kotlin.Any?)"
- "android.view.ViewGroup.findViewById(kotlin.Int)"
- "android.view.ViewGroup.getChildAt(kotlin.Int)"
Expand Down Expand Up @@ -1097,6 +1098,7 @@ datadog:
- "kotlin.collections.emptySet()"
- "kotlin.collections.listOf(android.view.Window)"
- "kotlin.collections.listOf(com.datadog.android.api.InternalLogger.Target)"
- "kotlin.collections.listOf(com.datadog.android.rum.internal.vitals.FPSVitalListener)"
- "kotlin.collections.listOf(com.datadog.android.rum.model.ActionEvent.Interface)"
- "kotlin.collections.listOf(com.datadog.android.rum.model.ErrorEvent.Interface)"
- "kotlin.collections.listOf(com.datadog.android.rum.model.LongTaskEvent.Interface)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.datadog.android.rum.internal.tracking.NoOpUserActionTrackingStrategy
import com.datadog.android.rum.internal.tracking.UserActionTrackingStrategy
import com.datadog.android.rum.internal.vitals.AggregatingVitalMonitor
import com.datadog.android.rum.internal.vitals.CPUVitalReader
import com.datadog.android.rum.internal.vitals.FPSVitalListener
import com.datadog.android.rum.internal.vitals.JankStatsActivityLifecycleListener
import com.datadog.android.rum.internal.vitals.MemoryVitalReader
import com.datadog.android.rum.internal.vitals.NoOpVitalMonitor
Expand Down Expand Up @@ -416,7 +417,9 @@ internal class RumFeature(
)

jankStatsActivityLifecycleListener = JankStatsActivityLifecycleListener(
frameRateVitalMonitor,
listOf(
FPSVitalListener(frameRateVitalMonitor)
),
sdkCore.internalLogger
)
(appContext as? Application)?.registerActivityLifecycleCallbacks(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.android.rum.internal.domain

import android.os.Build
import androidx.annotation.RequiresApi

internal data class FrameMetricsData(
@RequiresApi(Build.VERSION_CODES.N) var unknownDelayDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var inputHandlingDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var animationDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var layoutMeasureDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var drawDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var syncDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var commandIssueDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var swapBuffersDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var totalDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.N) var firstDrawFrame: Boolean = false,
@RequiresApi(Build.VERSION_CODES.O) var intendedVsyncTimestamp: Long = 0L,
@RequiresApi(Build.VERSION_CODES.O) var vsyncTimestamp: Long = 0L,
@RequiresApi(Build.VERSION_CODES.S) var gpuDuration: Long = 0L,
@RequiresApi(Build.VERSION_CODES.S) var deadline: Long = 0L,
var displayRefreshRate: Double = SIXTY_FPS
) {
companion object {
private const val SIXTY_FPS: Double = 60.0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.android.rum.internal.vitals

import android.os.Build
import androidx.metrics.performance.FrameData
import com.datadog.android.core.internal.system.BuildSdkVersionProvider
import com.datadog.android.rum.internal.domain.FrameMetricsData
import java.util.concurrent.TimeUnit

internal class FPSVitalListener(
private val vitalObserver: VitalObserver,
private val buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT,
private var screenRefreshRate: Double = 60.0
) : FrameStateListener {
private var frameDeadline = EXPECTED_60_FPS_FRAME_DURATION_NS
private var displayRefreshRate: Double = SIXTY_FPS

override fun onFrame(volatileFrameData: FrameData) {
val durationNs = volatileFrameData.frameDurationUiNanos
if (durationNs > 0.0) {
var frameRate = (ONE_SECOND_NS / durationNs)

if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
screenRefreshRate = ONE_SECOND_NS / frameDeadline
} else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
screenRefreshRate = displayRefreshRate
}

// If normalized frame rate is still at over 60fps it means the frame rendered
// quickly enough for the devices refresh rate.
frameRate = (frameRate * (SIXTY_FPS / screenRefreshRate)).coerceAtMost(MAX_FPS)

if (frameRate > MIN_FPS) {
vitalObserver.onNewSample(frameRate)
}
}
}

override fun onFrameMetricsData(data: FrameMetricsData) {
displayRefreshRate = data.displayRefreshRate
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
frameDeadline = data.deadline
}
}

companion object {
private const val EXPECTED_60_FPS_FRAME_DURATION_NS: Long = 16_666_666L
private val ONE_SECOND_NS: Double = TimeUnit.SECONDS.toNanos(1).toDouble()

private const val MIN_FPS: Double = 1.0
private const val MAX_FPS: Double = 60.0
private const val SIXTY_FPS: Double = 60.0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.android.rum.internal.vitals

import com.datadog.android.rum.internal.domain.FrameMetricsData

internal interface FrameMetricsDataListener {
fun onFrameMetricsData(data: FrameMetricsData)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
package com.datadog.android.rum.internal.vitals

import androidx.metrics.performance.JankStats

internal interface FrameStateListener : JankStats.OnFrameListener, FrameMetricsDataListener
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,17 @@ import androidx.metrics.performance.FrameData
import androidx.metrics.performance.JankStats
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.system.BuildSdkVersionProvider
import com.datadog.android.rum.internal.domain.FrameMetricsData
import java.lang.ref.WeakReference
import java.util.WeakHashMap
import java.util.concurrent.TimeUnit

/**
* Utility class listening to frame rate information.
*/
internal class JankStatsActivityLifecycleListener(
private val vitalObserver: VitalObserver,
private val delegates: List<FrameStateListener>,
private val internalLogger: InternalLogger,
private val jankStatsProvider: JankStatsProvider = JankStatsProvider.DEFAULT,
private var screenRefreshRate: Double = 60.0,
private var buildSdkVersionProvider: BuildSdkVersionProvider = BuildSdkVersionProvider.DEFAULT
) : ActivityLifecycleCallbacks, JankStats.OnFrameListener {

Expand All @@ -44,7 +43,8 @@ internal class JankStatsActivityLifecycleListener(
internal val activeActivities = WeakHashMap<Window, MutableList<WeakReference<Activity>>>()
internal var display: Display? = null
private var frameMetricsListener: DDFrameMetricsListener? = null
internal var frameDeadline = SIXTEEN_MS_NS

private val frameMetricsData = FrameMetricsData()

// region ActivityLifecycleCallbacks
@MainThread
Expand Down Expand Up @@ -143,7 +143,7 @@ internal class JankStatsActivityLifecycleListener(
if (activeActivities[activity.window].isNullOrEmpty()) {
activeWindowsListener.remove(activity.window)
activeActivities.remove(activity.window)
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N) {
unregisterMetricListener(activity.window)
}
}
Expand All @@ -154,24 +154,7 @@ internal class JankStatsActivityLifecycleListener(
// region JankStats.OnFrameListener

override fun onFrame(volatileFrameData: FrameData) {
val durationNs = volatileFrameData.frameDurationUiNanos
if (durationNs > 0.0) {
var frameRate = (ONE_SECOND_NS / durationNs)

if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
screenRefreshRate = ONE_SECOND_NS / frameDeadline
} else if (buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
screenRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS
}

// If normalized frame rate is still at over 60fps it means the frame rendered
// quickly enough for the devices refresh rate.
frameRate = (frameRate * (SIXTY_FPS / screenRefreshRate)).coerceAtMost(MAX_FPS)

if (frameRate > MIN_FPS) {
vitalObserver.onNewSample(frameRate)
}
}
delegates.forEach { it.onFrame(volatileFrameData) }
}

// endregion
Expand Down Expand Up @@ -216,7 +199,7 @@ internal class JankStatsActivityLifecycleListener(
@SuppressLint("NewApi")
@MainThread
private fun trackWindowMetrics(isKnownWindow: Boolean, window: Window, activity: Activity) {
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S && !isKnownWindow) {
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N && !isKnownWindow) {
registerMetricListener(window)
} else if (display == null && buildSdkVersionProvider.version == Build.VERSION_CODES.R) {
// Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have
Expand All @@ -226,14 +209,37 @@ internal class JankStatsActivityLifecycleListener(
}
}

@RequiresApi(Build.VERSION_CODES.S)
@RequiresApi(Build.VERSION_CODES.N)
private fun registerMetricListener(window: Window) {
if (frameMetricsListener == null) {
frameMetricsListener = DDFrameMetricsListener()
}
// todo RUM-8799: handler thread can be used instead
val handler = Handler(Looper.getMainLooper())
// Only hardware accelerated views can be tracked with metrics listener
if (window.peekDecorView()?.isHardwareAccelerated == true) {
val decorView = window.peekDecorView()

if (decorView == null) {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{ "Unable to attach JankStatsListener to window, decorView is null" }
)
return
}

// We need to postpone this operation because isHardwareAccelerated will return
// false until the view is attached to the window. Note that in this case main looper should be used
decorView.post {
// Only hardware accelerated views can be tracked with metrics listener
if (!decorView.isHardwareAccelerated) {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{ "Unable to attach JankStatsListener to window, decorView is not hardware accelerated" }
)
return@post
}

frameMetricsListener?.let { listener ->
try {
@Suppress("UnsafeThirdPartyFunctionCall") // Listener can't be null here
Expand All @@ -247,12 +253,6 @@ internal class JankStatsActivityLifecycleListener(
)
}
}
} else {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{ "Unable to attach JankStatsListener to window, decorView is null or not hardware accelerated" }
)
}
}

Expand All @@ -273,13 +273,40 @@ internal class JankStatsActivityLifecycleListener(
@RequiresApi(Build.VERSION_CODES.N)
inner class DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener {

@RequiresApi(Build.VERSION_CODES.S)
@RequiresApi(Build.VERSION_CODES.N)
override fun onFrameMetricsAvailable(
window: Window,
frameMetrics: FrameMetrics,
dropCountSinceLastInvocation: Int
) {
frameDeadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
delegates.forEach { it.onFrameMetricsData(frameMetricsData.update(frameMetrics)) }
}
}

@RequiresApi(Build.VERSION_CODES.N)
private fun FrameMetricsData.update(frameMetrics: FrameMetrics) = apply {
displayRefreshRate = display?.refreshRate?.toDouble() ?: SIXTY_FPS
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.N) {
unknownDelayDuration = frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)
inputHandlingDuration = frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)
animationDuration = frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)
layoutMeasureDuration = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)
drawDuration = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)
syncDuration = frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
commandIssueDuration = frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION)
swapBuffersDuration = frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
firstDrawFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == TRUE
}
@SuppressLint("InlinedApi")
if (buildSdkVersionProvider.version >= Build.VERSION_CODES.O) {
intendedVsyncTimestamp = frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)
vsyncTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
}
@SuppressLint("InlinedApi")
Comment on lines +305 to +310
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why these annotations are needed? If removed, what is the error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using buildSdkVersionProvider for OS version resolution. Android Studios's linter is not aware of it and highlighting inappropriate api usage:
image

if (buildSdkVersionProvider.version >= Build.VERSION_CODES.S) {
gpuDuration = frameMetrics.getMetric(FrameMetrics.GPU_DURATION)
deadline = frameMetrics.getMetric(FrameMetrics.DEADLINE)
}
}

Expand All @@ -292,12 +319,7 @@ internal class JankStatsActivityLifecycleListener(
" shouldn't happen."
internal const val JANK_STATS_TRACKING_DISABLE_ERROR =
"Failed to disable JankStats tracking"

private val ONE_SECOND_NS: Double = TimeUnit.SECONDS.toNanos(1).toDouble()

private const val MIN_FPS: Double = 1.0
private const val MAX_FPS: Double = 60.0
private const val SIXTY_FPS: Double = 60.0
private const val SIXTEEN_MS_NS: Long = 16666666
private const val TRUE = 1L
}
}
Loading
Loading