[RUM-2258] Improve vital support for higher refresh rate devices#1806
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## develop #1806 +/- ##
===========================================
- Coverage 83.49% 83.33% -0.16%
===========================================
Files 467 467
Lines 16491 16517 +26
Branches 2483 2492 +9
===========================================
- Hits 13769 13764 -5
- Misses 2043 2068 +25
- Partials 679 685 +6
|
| interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { | ||
| override fun onFrameMetricsAvailable( | ||
| window: Window, | ||
| frameMetrics: FrameMetrics, | ||
| dropCountSinceLastInvocation: Int | ||
| ) | ||
|
|
||
| var frameDeadline: Long |
There was a problem hiding this comment.
I believe this one can be removed
| interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { | |
| override fun onFrameMetricsAvailable( | |
| window: Window, | |
| frameMetrics: FrameMetrics, | |
| dropCountSinceLastInvocation: Int | |
| ) | |
| var frameDeadline: Long | |
| interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { | |
| var frameDeadline: Long |
| private fun registerMetricListener(window: Window, activity: Activity) { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| val handler = Handler(Looper.getMainLooper()) | ||
| window.addOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT, handler) | ||
| } else { | ||
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | ||
| // Frame Metrics callbacks available | ||
| if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { | ||
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | ||
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
latest version of ktlint we are using have indent formatting turned off (for a while, we have a ticket for it), so it is better to format code with Android Studio to fix the indent.
| private fun registerMetricListener(window: Window, activity: Activity) { | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | |
| val handler = Handler(Looper.getMainLooper()) | |
| window.addOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT, handler) | |
| } else { | |
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | |
| // Frame Metrics callbacks available | |
| if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { | |
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | |
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | |
| } | |
| } | |
| } | |
| private fun registerMetricListener(window: Window, activity: Activity) { | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | |
| val handler = Handler(Looper.getMainLooper()) | |
| window.addOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT, handler) | |
| } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && display == null) { | |
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | |
| // Frame Metrics callbacks available | |
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | |
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | |
| } | |
| } |
Also I think we may set display only once (if DisplayManager doesn't depend on a particular UI context though).
There was a problem hiding this comment.
I think not, although I didn't look through AOSP to see if on foldables the default screen changes depending on fold state.
| private fun unregisterMetricListener(window: Window) { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| window.removeOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT) | ||
| } | ||
| } |
There was a problem hiding this comment.
formatting:
| private fun unregisterMetricListener(window: Window) { | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | |
| window.removeOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT) | |
| } | |
| } | |
| private fun unregisterMetricListener(window: Window) { | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | |
| window.removeOnFrameMetricsAvailableListener(DDFrameMetricsListener.DEFAULT) | |
| } | |
| } |
| frameRate *= (SIXTY_FPS / screenRefreshRate) | ||
|
|
||
| // If normalized frame rate is still at over 60fps it means the frame rendered | ||
| // quickly enough for the devices refresh rate. | ||
| if (frameRate > MAX_FPS) { | ||
| frameRate = MAX_FPS | ||
| } |
There was a problem hiding this comment.
very minor, just looks shorter
| frameRate *= (SIXTY_FPS / screenRefreshRate) | |
| // If normalized frame rate is still at over 60fps it means the frame rendered | |
| // quickly enough for the devices refresh rate. | |
| if (frameRate > MAX_FPS) { | |
| frameRate = MAX_FPS | |
| } | |
| // If normalized frame rate is still at over 60fps it means the frame rendered | |
| // quickly enough for the devices refresh rate. | |
| frameRate *= (SIXTY_FPS / screenRefreshRate).coerceAtMost(MAX_FPS) |
| } | ||
|
|
||
| @RequiresApi(Build.VERSION_CODES.N) | ||
| interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { |
There was a problem hiding this comment.
we should move it to the bottom of the file, just before the companion, because the ordering is propeties > methods > inner classes/interfaces
| private var screenRefreshRate: Double = 60.0 | ||
| ) : ActivityLifecycleCallbacks, JankStats.OnFrameListener { | ||
|
|
There was a problem hiding this comment.
We can remove it from the constructor, I don't see any call sites where we pass custom screenRefreshRate at the JankStatsActivityLifecycleListener construction time
| private var screenRefreshRate: Double = 60.0 | |
| ) : ActivityLifecycleCallbacks, JankStats.OnFrameListener { | |
| ) : ActivityLifecycleCallbacks, JankStats.OnFrameListener { | |
| private var screenRefreshRate: Double = 60.0 |
There was a problem hiding this comment.
It is used in the test 𝕄 adjust sample value to refresh rate 𝕎 doFrame() {refresh rate over 60hz}
| } | ||
|
|
||
| @Test | ||
| fun `𝕄 adjust sample value to refresh rate 𝕎 doFrame() {refresh rate over 60hz}`( |
There was a problem hiding this comment.
ideally, it would be nice to see the tests for different Android API versions (since there are different code paths in the class under test depending on API version), but this may explode the number of tests, so it is not a blocker for me.
There was a problem hiding this comment.
We just need to test pre-R, R and post-R.
I can include those.
| @@ -65,6 +101,8 @@ internal class JankStatsActivityLifecycleListener( | |||
| activeWindowsListener[window] = jankStats | |||
There was a problem hiding this comment.
Not related with this PR but maybe we should add a task here to not use anymore the window as a key here. See the previous problems we had with using the activity as a key for view scopes.
| } | ||
|
|
||
| @RequiresApi(Build.VERSION_CODES.N) | ||
| interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { |
There was a problem hiding this comment.
any particular reason you need an interface here ? I think just a simple class would be simpler.
There was a problem hiding this comment.
Originally it was meant to be provided as parameter to aid in mocking, but ultimately didn't need to. I'll change it.
|
|
||
| internal val activeWindowsListener = WeakHashMap<Window, JankStats>() | ||
| internal val activeActivities = WeakHashMap<Window, MutableList<WeakReference<Activity>>>() | ||
| internal var display: Display? = null |
There was a problem hiding this comment.
we should not keep the Display as a reference here as it can create memory leaks.
There was a problem hiding this comment.
if it is not bound to the particular UI-context and there is just a single instance for the whole app, it should be fine I guess.
There was a problem hiding this comment.
That was my assumption, I can replace with a weak reference if you prefer.
There was a problem hiding this comment.
if you are certain that it will be the main window display instance then it will be fine.
| const val ONE_SECOND_NS: Long = 1000L * 1000L * 1000L | ||
| const val TEN_SECOND_NS: Long = 10L * ONE_SECOND_NS | ||
| const val ONE_MINUTE_NS: Long = 60L * ONE_SECOND_NS | ||
| const val MIN_FPS: Double = 1.0 |
There was a problem hiding this comment.
minor: If we in the future modify min/max fps we would have to change this in two places. We could make the class variables internal to share with the test
There was a problem hiding this comment.
I think it's unlikely to change, but I can follow this pattern if we're using it elsewhere.
mariusc83
left a comment
There was a problem hiding this comment.
I am going to change to Comment from Request Changes to not block you while I am in PTO
676e934 to
81b6565
Compare
0xnm
left a comment
There was a problem hiding this comment.
I've added some comments/thoughts, let me know what you think. But overall the change is lgtm.
| } else { | ||
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | ||
| // Frame Metrics callbacks available | ||
| if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { | ||
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | ||
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
seems formatting is a bit off here (ktlint in the current state has indent rule disabled, so it doesn't fix/verify it):
| } else { | |
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | |
| // Frame Metrics callbacks available | |
| if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { | |
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | |
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | |
| } | |
| } | |
| } | |
| } else { | |
| // Fallback - Android 30 allows apps to not run at a fixed 60hz, but didn't yet have | |
| // Frame Metrics callbacks available | |
| if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { | |
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | |
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | |
| } | |
| } | |
| } |
better to re-format this file with Android Studio.
| activeActivities[window] = list | ||
| } | ||
|
|
||
| @RequiresApi(Build.VERSION_CODES.N) |
There was a problem hiding this comment.
It seems that it should be Build.VERSION_CODES.R here? because I can see 2 logical branches here: one which is executed only for Build.VERSION_CODES.S and above and another for Build.VERSION_CODES.R.
Or we just rely on the lowest API required from the methods description?
There was a problem hiding this comment.
I was relying on the API required for the method itself, just to be on the safe side.
But I don't have a strong opinion here, I can change it to .R if we feel it's better.
| if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { | ||
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | ||
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) | ||
| } |
There was a problem hiding this comment.
maybe we can pull this out from this method? it may be confusing, because there is no listener registration in this block. Also by pulling it out we will have a symmetry at the calls site between registerMetricListener and unregisterMetricListener calls in terms of the build version required, because currently the former is guarded by Build.VERSION.SDK_INT >= Build.VERSION_CODES.R and the latter Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - this raises the obvious question initially: we don't unregister listener registered if Build.VERSION.SDK_INT == Build.VERSION_CODES.R?
After this change, the following:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
registerMetricListener(window, activity)
}
will become:
if (buildSdkVersionProvider.version() >= Build.VERSION_CODES.S) {
registerMetricListener(window, activity)
} else if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) {
val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
}
which is cleaner to me
| // Frame Metrics callbacks available | ||
| if (display == null && buildSdkVersionProvider.version() == Build.VERSION_CODES.R) { | ||
| val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | ||
| display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) |
There was a problem hiding this comment.
I believe holding Display object shouldn't cause memory leaks by looking on its structure https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/Display.java;l=1?q=Display.java&sq= - it seems it doesn't have references to the UI-related context (Activity), the only concern is Resources object which may hold such reference. So maybe it is worth to check this change for the leaks.
There was a problem hiding this comment.
Didn't see anything on my local profiles.
Also did a quick analysis on the activity allocation/deallocation pattern in the sample app with and without these changes and found no difference.
81b6565 to
8873500
Compare
What does this PR do?
CPU Vitals start to take into account refresh rate of device obtained either through
OnFrameMetricsAvailableListenerorDisplay.getRefreshRate().Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)