| /* |
| * 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). |
| */ |
| 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.getHolderForHierarchy(view) |
| |
| // stateInfo is the backing store for the list of states that are active on any given |
| // frame. It is passed to the JankStats listeners as part of the FrameData structure. |
| // Reusing this mutable version of it enables zero-allocation metrics reporting. |
| val stateInfo = mutableListOf<StateInfo>() |
| |
| // frameData is reused every time, populated with the latest frame's data before |
| // sending out to listeners. Reuse enables zero-allocation metrics reporting. |
| private val frameData = FrameData(0, 0, false, stateInfo) |
| |
| /** |
| * 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 { |
| if (enable) { |
| val delegates = decorView.getOrCreateOnPreDrawListenerDelegator() |
| delegates.add(onFrameListenerDelegate) |
| } else { |
| decorView.removeOnPreDrawListenerDelegate(onFrameListenerDelegate) |
| } |
| } |
| } |
| |
| internal open fun getFrameData( |
| startTime: Long, |
| uiDuration: Long, |
| expectedDuration: Long |
| ): FrameData { |
| metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration, |
| stateInfo) |
| val isJank = uiDuration > expectedDuration |
| frameData.update(startTime, uiDuration, isJank) |
| return frameData |
| } |
| |
| private fun View.removeOnPreDrawListenerDelegate(delegate: OnFrameListenerDelegate) { |
| val delegator = getTag(R.id.metricsDelegator) as DelegatingOnPreDrawListener? |
| delegator?.remove(delegate, viewTreeObserver) |
| } |
| |
| /** |
| * 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.getOrCreateOnPreDrawListenerDelegator(): DelegatingOnPreDrawListener { |
| 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 |
| } |
| |
| 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) |
| } |
| } |
| |
| /** |
| * 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 { |
| |
| // Track whether the delegate list is being iterated, used to prevent concurrent modification |
| var iterating = false |
| |
| // These lists cache add/remove requests to be handled after the current iteration loop |
| val toBeAdded = mutableListOf<OnFrameListenerDelegate>() |
| val toBeRemoved = mutableListOf<OnFrameListenerDelegate>() |
| |
| val decorViewRef = WeakReference<View>(decorView) |
| val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(decorView) |
| |
| /** |
| * It is possible for the delegates list to be modified concurrently (adding/removing items |
| * while also iterating through the list). To prevent this, we synchronize on this instance. |
| * It is also possible for the same thread to do both operations, causing reentrance into |
| * that synchronization block. However, the only way that should happen is if the list is |
| * being iterated on (which is called from the UI thread) and, in any of those delegate |
| * listeners, the delegates list is modified |
| * (by calling JankStats.isTrackingEnabled()). In this case, we cache the request in one of the |
| * toBeAdded/Removed lists and return. When iteration is complete, we handle those requests. |
| * This would not be sufficient if those operations could happen randomly on the same thread, |
| * but the order should also be as described above (with add/remove nested inside iteration). |
| * |
| * Iteration and add/remove could also happen randomly and concurrently on different threads, |
| * but in that case the synchronization block around both accesses should suffice. |
| */ |
| |
| override fun onPreDraw(): Boolean { |
| val decorView = decorViewRef.get() |
| decorView?.let { |
| val frameStart = getFrameStartTime() |
| with(decorView) { |
| handler.sendMessageAtFrontOfQueue(Message.obtain(handler) { |
| val now = System.nanoTime() |
| val expectedDuration = getExpectedFrameDuration(decorView) |
| // prevent concurrent modification of delegates list by synchronizing on |
| // this delegator object while iterating and modifying |
| synchronized(this@DelegatingOnPreDrawListener) { |
| iterating = true |
| for (delegate in delegates) { |
| delegate.onFrame(frameStart, now - frameStart, expectedDuration) |
| } |
| if (toBeAdded.isNotEmpty()) { |
| for (delegate in toBeAdded) { |
| delegates.add(delegate) |
| } |
| toBeAdded.clear() |
| } |
| if (toBeRemoved.isNotEmpty()) { |
| val delegatesNonEmpty = delegates.isNotEmpty() |
| for (delegate in toBeRemoved) { |
| delegates.remove(delegate) |
| } |
| toBeRemoved.clear() |
| // Only remove delegator if we emptied the list here |
| if (delegatesNonEmpty && delegates.isEmpty()) { |
| viewTreeObserver.removeOnPreDrawListener( |
| this@DelegatingOnPreDrawListener |
| ) |
| setTag(R.id.metricsDelegator, null) |
| } |
| } |
| iterating = false |
| } |
| metricsStateHolder.state?.cleanupSingleFrameStates() |
| }.apply { |
| setMessageAsynchronicity(this) |
| }) |
| } |
| } |
| return true |
| } |
| |
| fun add(delegate: OnFrameListenerDelegate) { |
| // prevent concurrent modification of delegates list by synchronizing on |
| // this delegator object while iterating and modifying |
| synchronized(this) { |
| if (iterating) { |
| toBeAdded.add(delegate) |
| } else { |
| delegates.add(delegate) |
| } |
| } |
| } |
| |
| fun remove(delegate: OnFrameListenerDelegate, viewTreeObserver: ViewTreeObserver) { |
| // prevent concurrent modification of delegates list by synchronizing on |
| // this delegator object while iterating and modifying |
| synchronized(this) { |
| if (iterating) { |
| toBeRemoved.add(delegate) |
| } else { |
| val delegatesNonEmpty = delegates.isNotEmpty() |
| delegates.remove(delegate) |
| // Only remove delegator if we emptied the list here |
| if (delegatesNonEmpty && delegates.isEmpty()) { |
| viewTreeObserver.removeOnPreDrawListener(this) |
| val decorView = decorViewRef.get() |
| decorView?.setTag(R.id.metricsDelegator, null) |
| } else { |
| // noop - compiler requires else{} clause here for some strange reason |
| } |
| } |
| } |
| } |
| 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 |
| } |
| } |
| } |