-
Notifications
You must be signed in to change notification settings - Fork 133
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
RUMM-3222 Use targetTimestamp as reference to calculate FPS #1272
Conversation
Datadog ReportBranch report: ✅ |
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.
LGTM!
One tiny suggestion, and one question about expanding the support for 60+ FPS on the backend.
// ProMotion displays (e.g. iPad Pro and newer iPhone Pro) can have refresh rate higher than 60 FPS. | ||
if let expectedCurrentFrameDuration = self.nextFrameDuration, provider.maximumDeviceFramesPerSecond > 60 { | ||
let expectedFPS = 1.0 / expectedCurrentFrameDuration | ||
fps = currentFPS * (60.0 / expectedFPS) |
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.
Quote from the blog:
Since devices can have [different maximum refresh rates](https://developer.android.com/guide/topics/media/frame-rate), Mobile Vitals scales this metric to a value between zero and 60, allowing you to track your application’s refresh rate regardless of hardware-specific restrictions.
I wonder if normalisation makes sense though. The FPS value is completely wrong in that case (although gives the sense of performance). I don't fully understand why we couldn't send both current FPS and the baseline to the backend.
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.
yeah, in ideal world, we would want to have a graph with two values on a time scale.
Expected frame rate and real frame rate but we are not designed for this (as of now).
My plan to start a conversion with backed to allow sending metrics that have such type of measurement scale.
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.
Looks good and it reads very well 💪👍. I left few suggestions and (perhaps), flakiness alert ❄️
Tests/DatadogTests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift
Show resolved
Hide resolved
### What and why? Currently, to find out the FPS of a view, the SDK look for the frame duration and inverses it to get the FPS. Eg. if the frame duration is 16ms (0.016s), the FPS is 1/0.016 = 60FPS (rounded). This generally works but when comes to variable refresh rate displays, it becomes an issue. For example, let's say OS has decided to increase the frame duration to 32ms to save battery, the FPS will be 30FPS but that doesn't mean the app is having a poor UI performance. It can very well be that the app is doing nothing. ### How? `CADisplayLink` has a property called `targetTimestamp` which is the time when the next frame is scheduled to be displayed. This is the time when the frame is actually displayed on the screen. Hence, the SDK uses this property to calculate the expected frame duration and then inverses it to get the FPS. At the same time, SDK continues to use the old logic to calculate the FPS using the `timestamp` property. Finally, the SDK normalizes the FPS to 60FPS as Datadog backend only supports 60FPS rate for now described [here](https://www.datadoghq.com/blog/monitor-mobile-vitals-datadog/#slow-rendering).
0e201fc
to
d88e9bf
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.
Looks great!
Thanks for the CHANGELOG, tho it needs an extra line for linking the PR. I also left a minor suggestion, but it's good to merge as is 👍
var fps: Double? = nil | ||
|
||
if let lastFrameTimestamp = self.lastFrameTimestamp { | ||
let currentFrameDuration = provider.currentFrameTimestamp - lastFrameTimestamp | ||
guard currentFrameDuration > 0 else { | ||
return nil | ||
} | ||
let currentFPS = 1.0 / currentFrameDuration | ||
|
||
// ProMotion displays (e.g. iPad Pro and newer iPhone Pro) can have refresh rate higher than 60 FPS. | ||
if let expectedCurrentFrameDuration = self.nextFrameDuration, provider.adaptiveFrameRateSupported { | ||
guard expectedCurrentFrameDuration > 0 else { | ||
return nil | ||
} | ||
let expectedFPS = 1.0 / expectedCurrentFrameDuration | ||
fps = currentFPS * (Self.backendSupportedFrameRate / expectedFPS) | ||
} else { | ||
fps = currentFPS | ||
} | ||
} | ||
|
||
self.lastFrameTimestamp = provider.currentFrameTimestamp | ||
self.nextFrameDuration = provider.nextFrameTimestamp - provider.currentFrameTimestamp | ||
|
||
return 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.
/nit/suggestion
The nested statements make it difficult to catch, maybe we could flatten these by using a defer
, e.g:
defer {
self.lastFrameTimestamp = provider.currentFrameTimestamp
self.nextFrameDuration = provider.nextFrameTimestamp - provider.currentFrameTimestamp
}
guard let lastFrameTimestamp = self.lastFrameTimestamp else {
return nil
}
let currentFrameDuration = provider.currentFrameTimestamp - lastFrameTimestamp
guard currentFrameDuration > 0 else {
return nil
}
let currentFPS = 1.0 / currentFrameDuration
// ProMotion displays (e.g. iPad Pro and newer iPhone Pro) can have refresh rate higher than 60 FPS.
guard provider.adaptiveFrameRateSupported, let expectedCurrentFrameDuration = self.nextFrameDuration else {
return currentFPS
}
guard expectedCurrentFrameDuration > 0 else {
return nil
}
let expectedFPS = 1.0 / expectedCurrentFrameDuration
return currentFPS * (Self.backendSupportedFrameRate / expectedFPS)
If I got it right :)
What and why?
Currently, to find out the FPS of a view, the SDK look for the frame duration and inverses it to get the FPS.
Eg. if the frame duration is 16ms (0.016s), the FPS is 1/0.016 = 60FPS (rounded).
This generally works but when comes to variable refresh rate displays, it becomes an issue. For example, let's say OS has decided to increase the frame duration to 32ms to save battery, the FPS will be 30FPS but that doesn't mean the app is having a poor UI performance. It can very well be that the app is doing nothing.
How?
CADisplayLink
has a property calledtargetTimestamp
which is the time when the next frame is scheduled to be displayed. This is the time when the frame is actually displayed on the screen. Hence, the SDK uses this property to calculate the expected frame duration and then inverses it to get the FPS.At the same time, SDK continues to use the old logic to calculate the FPS using the
timestamp
property.Finally, the SDK normalizes the FPS to 60FPS as Datadog backend only supports 60FPS rate for now described here.
Review checklist
Custom CI job configuration (optional)