-
Notifications
You must be signed in to change notification settings - Fork 61
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-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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
|
||
// region ActivityLifecycleCallbacks | ||
@MainThread | ||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | ||
} | ||
|
||
@RequiresApi(Build.VERSION_CODES.N) | ||
interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is used in the test 𝕄 adjust sample value to refresh rate 𝕎 doFrame() {refresh rate over 60hz}
@@ -356,10 +359,41 @@ internal class JankStatsActivityLifecycleListenerTest { | |||
verify(mockObserver, never()).onNewSample(any()) | |||
} | |||
|
|||
@Test | |||
fun `𝕄 adjust sample value to refresh rate 𝕎 doFrame() {refresh rate over 60hz}`( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
||
// region ActivityLifecycleCallbacks | ||
@MainThread | ||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | ||
} | ||
|
||
@RequiresApi(Build.VERSION_CODES.N) | ||
interface DDFrameMetricsListener : Window.OnFrameMetricsAvailableListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any particular reason you need an interface here ? I think just a simple class would be simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally it was meant to be provided as parameter to aid in mocking, but ultimately didn't need to. I'll change it.
) : ActivityLifecycleCallbacks, JankStats.OnFrameListener { | ||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should not keep the Display
as a reference here as it can create memory leaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my assumption, I can replace with a weak reference if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you are certain that it will be the main window display
instance then it will be fine.
companion object { | ||
const val ONE_MILLISECOND_NS: Long = 1000L * 1000L | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's unlikely to change, but I can follow this pattern if we're using it elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to change to Comment
from Request Changes
to not block you while I am in PTO
676e934
to
81b6565
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -160,6 +195,42 @@ internal class JankStatsActivityLifecycleListener( | |||
activeActivities[window] = list | |||
} | |||
|
|||
@RequiresApi(Build.VERSION_CODES.N) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
OnFrameMetricsAvailableListener
orDisplay.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)