JankStatsApi24Impl.kt
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.metrics.performance
import android.os.Handler
import android.os.HandlerThread
import android.view.FrameMetrics
import android.view.View
import android.view.Window
import androidx.annotation.RequiresApi
/**
* Subclass of JankStatsBaseImpl records frame timing data for API 24 and later,
* using FrameMetrics (which was introduced in API 24). Jank data is collected by
* setting a [Window.addOnFrameMetricsAvailableListener]
* on the Window associated with the Activity being tracked.
*/
@RequiresApi(24)
internal open class JankStatsApi24Impl(
jankStats: JankStats,
view: View,
private val window: Window
) : JankStatsApi22Impl(jankStats, view) {
// Workaround for situation like b/206956036, where platform would sometimes send completely
// duplicate events through FrameMetrics. When that occurs, simply ignore the latest event
// that has the exact same start time.
var prevStart = 0L
// Used to avoid problems with data gathered before things are set up
var listenerAddedTime: Long = 0
private val frameMetricsAvailableListenerDelegate: Window.OnFrameMetricsAvailableListener =
Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ ->
val startTime = getFrameStartTime(frameMetrics)
// ignore historical data gathered before we started listening
if (startTime >= listenerAddedTime && startTime != prevStart) {
val expectedDuration = getExpectedFrameDuration(frameMetrics) *
jankStats.jankHeuristicMultiplier
jankStats.logFrameData(getFrameData(startTime, expectedDuration.toLong(),
frameMetrics))
prevStart = startTime
}
}
internal open fun getFrameData(
startTime: Long,
expectedDuration: Long,
frameMetrics: FrameMetrics
): FrameDataApi24 {
val uiDuration = frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION) +
frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION) +
frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION) +
frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) +
frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) +
frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
val frameStates =
metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration)
?: emptyList()
val isJank = uiDuration > expectedDuration
val cpuDuration = uiDuration +
frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION) +
frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
return FrameDataApi24(startTime, uiDuration, cpuDuration, isJank, frameStates)
}
internal open fun getFrameStartTime(frameMetrics: FrameMetrics): Long {
return getFrameStartTime()
}
open fun getExpectedFrameDuration(metrics: FrameMetrics): Long {
return getExpectedFrameDuration(decorViewRef.get())
}
override fun setupFrameTimer(enable: Boolean) {
window.let {
if (enable) {
if (listenerAddedTime == 0L) {
val delegates = window.getOrCreateFrameMetricsListenerDelegates()
delegates.add(frameMetricsAvailableListenerDelegate)
listenerAddedTime = System.nanoTime()
}
} else {
window.removeFrameMetricsListenerDelegate(frameMetricsAvailableListenerDelegate)
listenerAddedTime = 0
}
}
}
companion object {
// Need a Handler for FrameMetricsListener; just use a singleton, no need for Thread
// overhead per JankStats instance
internal var frameMetricsHandler: Handler? = null
}
@RequiresApi(24)
private fun Window.removeFrameMetricsListenerDelegate(
delegate: Window.OnFrameMetricsAvailableListener
) {
val delegator = decorView.getTag(R.id.metricsDelegator) as
DelegatingFrameMetricsListener?
with(delegator?.delegates) {
this?.remove(delegate)
if (this?.size == 0) {
removeOnFrameMetricsAvailableListener(delegator)
decorView.setTag(R.id.metricsDelegator, null)
}
}
}
/**
* This function returns the current list of FrameMetricsListener delegates.
* If no such list exists, it will create it, and add a root listener which
* delegates to that list.
*/
@RequiresApi(24)
private fun Window.getOrCreateFrameMetricsListenerDelegates():
MutableList<Window.OnFrameMetricsAvailableListener> {
var delegator = decorView.getTag(R.id.metricsDelegator) as
DelegatingFrameMetricsListener?
if (delegator == null) {
val delegates = mutableListOf<Window.OnFrameMetricsAvailableListener>()
delegator = DelegatingFrameMetricsListener(delegates)
// First listener for this window; create the delegates list and
// add a listener to the window
if (frameMetricsHandler == null) {
val thread = HandlerThread("FrameMetricsAggregator")
thread.start()
frameMetricsHandler = Handler(thread.looper)
}
addOnFrameMetricsAvailableListener(delegator,
frameMetricsHandler
)
decorView.setTag(R.id.metricsDelegator, delegator)
}
return delegator.delegates
}
}
/**
* To avoid having multiple frame metrics listeners for a given window (if the client
* creates multiple JankStats instances on that window), we use a single listener and
* delegate out to the multiple listeners provided by the client. This single instance
* and the list of delegates are cached in view tags in the DecorView for the window.
*/
@RequiresApi(24)
private class DelegatingFrameMetricsListener(
val delegates: MutableList<Window.OnFrameMetricsAvailableListener>
) : Window.OnFrameMetricsAvailableListener {
override fun onFrameMetricsAvailable(
window: Window?,
frameMetrics: FrameMetrics?,
dropCount: Int
) {
for (delegate in delegates) {
delegate.onFrameMetricsAvailable(window, frameMetrics, dropCount)
}
// Remove singleFrame states now that we are done processing this frame
if (window?.decorView != null) {
val holder = PerformanceMetricsState.getForHierarchy(window.decorView)
holder.state?.cleanupSingleFrameStates()
}
}
}