/*
* 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.app.Activity
import android.os.Message
import android.view.Choreographer
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.RequiresApi
import java.lang.ref.WeakReference
import java.lang.reflect.Field
/**
* Subclass of JankStatsBaseImpl records frame timing data for API 16 and later,
* using Choreographer (which was introduced in API 16).
*/
@RequiresApi(16)
internal open class JankStatsApi16Impl(
jankStats: JankStats,
view: View
) : JankStatsBaseImpl(jankStats) {
// TODO: decorView may change in Window, think about how to handle that
// e.g., should we cache Window instead?
internal val decorViewRef: WeakReference<View> = WeakReference(view)
// Must cache this at init time, from view, since some subclasses will not receive callbacks
// on the UI thread, so they will not have access to the appropriate Choreographer for
// frame timing values
val choreographer: Choreographer = Choreographer.getInstance()
// Cache for use during reporting, to supply the FrameData states
val metricsStateHolder = PerformanceMetricsState.getForHierarchy(view)
/**
* Each JankStats instance has its own listener for per-frame metric data.
* But we use a single listener (using OnPreDraw events prior to API 24) to gather
* the frame data, and then delegate that information to all instances.
* OnFrameListenerDelegate is the object that the per-frame data is delegated to,
* which forwards it to the JankStats instances.
*/
private val onFrameListenerDelegate = object : OnFrameListenerDelegate() {
override fun onFrame(startTime: Long, uiDuration: Long, expectedDuration: Long) {
jankStats.logFrameData(getFrameData(startTime, uiDuration,
(expectedDuration * jankStats.jankHeuristicMultiplier).toLong()))
}
}
override fun setupFrameTimer(enable: Boolean) {
val decorView = decorViewRef.get()
decorView?.let {
withDelegates(decorView) {
if (enable) {
val delegates = decorView.getOrCreateOnPreDrawListenerDelegates()
delegates.add(onFrameListenerDelegate)
} else {
decorView.removeOnPreDrawListenerDelegate(onFrameListenerDelegate)
}
}
}
}
internal open fun getFrameData(
startTime: Long,
uiDuration: Long,
expectedDuration: Long
): FrameData {
val frameStates =
metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration)
?: emptyList()
val isJank = uiDuration > expectedDuration
return FrameData(startTime, uiDuration, isJank, frameStates)
}
private fun View.removeOnPreDrawListenerDelegate(delegate: OnFrameListenerDelegate) {
val delegator = getTag(R.id.metricsDelegator) as DelegatingOnPreDrawListener?
with(delegator?.delegates) {
this?.remove(delegate)
if (this?.size == 0) {
viewTreeObserver.removeOnPreDrawListener(delegator)
setTag(R.id.metricsDelegator, null)
}
}
}
/**
* This function returns the current list of OnPreDrawListener delegates.
* If no such list exists, it will create it and add a root listener that
* delegates to that list.
*/
private fun View.getOrCreateOnPreDrawListenerDelegates():
MutableList<OnFrameListenerDelegate> {
var delegator = getTag(R.id.metricsDelegator) as DelegatingOnPreDrawListener?
if (delegator == null) {
val delegates = mutableListOf<OnFrameListenerDelegate>()
delegator = createDelegatingOnDrawListener(this, choreographer, delegates)
viewTreeObserver.addOnPreDrawListener(delegator)
setTag(R.id.metricsDelegator, delegator)
}
return delegator.delegates
}
internal open fun createDelegatingOnDrawListener(
view: View,
choreographer: Choreographer,
delegates: MutableList<OnFrameListenerDelegate>
): DelegatingOnPreDrawListener {
return DelegatingOnPreDrawListener(view, choreographer, delegates)
}
internal fun getFrameStartTime(): Long {
return DelegatingOnPreDrawListener.choreographerLastFrameTimeField.get(choreographer)
as Long
}
fun getExpectedFrameDuration(view: View?): Long {
return DelegatingOnPreDrawListener.getExpectedFrameDuration(view)
}
companion object {
/**
* Anything dealing with the delegates list must go through this function, to avoid the list
* changing while being used on a different thread. It synchronizes on the DecorView object
* to guarantee that all delegate access is similarly synchronized.
*/
fun withDelegates(decorView: View, delegateAction: () -> Unit) {
// prevent concurrent modification of delegates list by synchronizing on
// DecorView, which holds the delegator object
synchronized(decorView) {
delegateAction()
}
}
}
}
/**
* This class is used by DelegatingOnDrawListener, which calculates the frame timing values
* and calls all delegate listeners with that data.
*/
internal abstract class OnFrameListenerDelegate {
abstract fun onFrame(startTime: Long, uiDuration: Long, expectedDuration: Long)
}
/**
* There is only a single listener for OnPreDraw events, which are used to calculate frame
* timing details. This listener delegates to a list of OnFrameListenerDelegate objects,
* which do the work of sending that data to JankStats instance clients.
*/
@RequiresApi(16)
internal open class DelegatingOnPreDrawListener(
decorView: View,
val choreographer: Choreographer,
val delegates: MutableList<OnFrameListenerDelegate>
) : ViewTreeObserver.OnPreDrawListener {
val decorViewRef = WeakReference<View>(decorView)
val metricsStateHolder = PerformanceMetricsState.getForHierarchy(decorView)
override fun onPreDraw(): Boolean {
val decorView = decorViewRef.get()
decorView?.let {
val frameStart = getFrameStartTime()
with(decorView) {
handler.sendMessage(Message.obtain(handler) {
val now = System.nanoTime()
val expectedDuration = getExpectedFrameDuration(decorView)
JankStatsApi16Impl.withDelegates(decorView) {
for (delegate in delegates) {
delegate.onFrame(frameStart, now - frameStart, expectedDuration)
}
}
metricsStateHolder.state?.cleanupSingleFrameStates()
}.apply {
setMessageAsynchronicity(this)
})
}
}
return true
}
private fun getFrameStartTime(): Long {
return choreographerLastFrameTimeField.get(choreographer) as Long
}
// Noop prior to API 22 - overridden in 22Impl subclass
internal open fun setMessageAsynchronicity(message: Message) {}
companion object {
val choreographerLastFrameTimeField: Field =
Choreographer::class.java.getDeclaredField("mLastFrameTimeNanos")
init {
choreographerLastFrameTimeField.isAccessible = true
}
@Suppress("deprecation") /* defaultDisplay */
fun getExpectedFrameDuration(view: View?): Long {
if (JankStatsBaseImpl.frameDuration < 0) {
var refreshRate = 60f
val window = if (view?.context is Activity)
(view.context as Activity).window else null
if (window != null) {
val display = window.windowManager.defaultDisplay
refreshRate = display.refreshRate
}
if (refreshRate < 30f || refreshRate > 200f) {
// Account for faulty return values (including 0)
refreshRate = 60f
}
JankStatsBaseImpl.frameDuration =
(1000 / refreshRate * JankStatsBaseImpl.NANOS_PER_MS).toLong()
}
return JankStatsBaseImpl.frameDuration
}
}
}