Feat: App start up and slow/frozen frames metrics#1466
Feat: App start up and slow/frozen frames metrics#1466marandaneto wants to merge 12 commits intomainfrom
Conversation
| // private boolean isHardwareAccelerated(Activity activity) { | ||
| // // we can't observe frame rates for a non hardware accelerated view | ||
| // return activity.getWindow() != null | ||
| // && ((activity.getWindow().getAttributes().flags | ||
| // & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) | ||
| // != 0); | ||
| // } |
There was a problem hiding this comment.
I read that reading slow and frozen frames are only possible if FLAG_HARDWARE_ACCELERATED is enabled, but it worked with it disabled during tests, so I'm not sure if I should keep this check or not.
FLAG_HARDWARE_ACCELERATED is disabled by default by the way
There was a problem hiding this comment.
Did you try it in a release build?
There was a problem hiding this comment.
yes, maybe thats the case for some OS versions only, I could not find anything though
sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java
Show resolved
Hide resolved
| final Map<String, @NotNull MeasurementValue> framesMetrics = | ||
| ActivityFramesState.getInstance().getMetrics(); | ||
| if (framesMetrics != null) { | ||
| transaction.getMeasurements().putAll(framesMetrics); |
There was a problem hiding this comment.
slow/frozen frames are always sent, to every transaction, and represents the overall amount of the app lifecycle, and not from the start-end time of the transaction, we can't track only during the start/end of the transaction unless we implement the whole counting ourselves and this changes based on OS version and so on, that's the abstraction the class FrameMetricsAggregator does
There was a problem hiding this comment.
Whenever we start a transaction, we could check the current count of the frames in ActivityFramesState. When you finish a transaction, you again call ActivityFramesState, and then you can calculate the different types of frames during the time of the transaction.
There was a problem hiding this comment.
that would not scale well if I have multiple transactions at the same time, the same issue as counting on iOS the frames on iOS, we can do it next maybe?
There was a problem hiding this comment.
Running into a similar issue on RN as well. Considered @philipphofmann's suggestion at first but on JS users can change the start/end timestamps which makes it not work and way more difficult to implement metrics at the start/end of the transaction.
There was a problem hiding this comment.
Why do we run into scaling problems? Hmm, changing it afterward might cause problems. Having it different on platforms is also not a good idea. The advantage of always sending the total number of frames during the lifetime of an application gives you more insights on how many slow frames and frozen frames you have, but the downside is that you have no clue where they are coming from. Being able to correlate them with transactions gives you way more insight. Maybe it would even make sense to send both lifetime frames and frames during the transaction.
sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java
Show resolved
Hide resolved
|
@philipphofmann and @bruno-garcia can you give some early feedback? I have left a few comments and TODO with my findings as well |
Codecov Report
@@ Coverage Diff @@
## main #1466 +/- ##
============================================
- Coverage 75.75% 75.71% -0.05%
Complexity 1926 1926
============================================
Files 190 191 +1
Lines 6580 6585 +5
Branches 665 665
============================================
+ Hits 4985 4986 +1
- Misses 1270 1274 +4
Partials 325 325
Continue to review full report at Codecov.
|
philipphofmann
left a comment
There was a problem hiding this comment.
Good job for a draft PR. Although it's a draft PR, I added many comments. I guess you would anyway fix most of them before making this PR ready for review. Still, I added them to not forget about them.
| <provider | ||
| android:name=".SentryPerformanceProvider" | ||
| android:authorities="${applicationId}.SentryPerformanceProvider" | ||
| android:initOrder="200" |
There was a problem hiding this comment.
😁. No just kidding.
| android:initOrder="200" | |
| android:initOrder="1" |
There was a problem hiding this comment.
the higher the more important, so it'd make sense to put Long.MAX_VALUE :D
| // frozen frames, threshold is 700ms | ||
| frozenFrames += numFrames; | ||
| } | ||
| if (frameTime > 16) { |
There was a problem hiding this comment.
m: With 60 FPS a frame actually can take 1000 / 60 = 16.66667 ms to render. On Cocoa, I had to change it to 1000 / 59, because almost every frame takes a few nanoseconds longer than the optimal duration. Is there a way to retrieve the current frame rate? It could also be 120 or 90.
There was a problem hiding this comment.
I don't think it changes on Android, it's written in their docs and source code in the same way
There was a problem hiding this comment.
Interesting. How does it work then if you have a phone with 120 fps?
There was a problem hiding this comment.
afaik Android displays are limited to 60 fps
sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java
Show resolved
Hide resolved
| return appStartTime; | ||
| } | ||
|
|
||
| synchronized void setAppStartTime() { |
There was a problem hiding this comment.
m: We could use Double-Checked Locking to avoid acquiring unnecessary locks. It might make sense to add a comment on why we need synchronization. I guess because someone could initialize the SDK on a background thread?
| // private boolean isHardwareAccelerated(Activity activity) { | ||
| // // we can't observe frame rates for a non hardware accelerated view | ||
| // return activity.getWindow() != null | ||
| // && ((activity.getWindow().getAttributes().flags | ||
| // & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) | ||
| // != 0); | ||
| // } |
There was a problem hiding this comment.
Did you try it in a release build?
| final Map<String, @NotNull MeasurementValue> framesMetrics = | ||
| ActivityFramesState.getInstance().getMetrics(); | ||
| if (framesMetrics != null) { | ||
| transaction.getMeasurements().putAll(framesMetrics); |
There was a problem hiding this comment.
Whenever we start a transaction, we could check the current count of the frames in ActivityFramesState. When you finish a transaction, you again call ActivityFramesState, and then you can calculate the different types of frames during the time of the transaction.
| // this is called after the Activity is created, so we know if the App is a warm or cold | ||
| // start. |
There was a problem hiding this comment.
l: Can you maybe elaborate a bit on why this is called after the Activity is created? Why don't we use onActivityCreated instead?
There was a problem hiding this comment.
I will add a better comment, but it's hard to explain and requires understanding deeply how the whole process start work, its a few pages that cant be easily resumed in one liner
There was a problem hiding this comment.
Can we maybe link to resources so you can find out on your own even when you have to read some articles?
|
|
||
| @Override | ||
| public synchronized void onActivityStarted(final @NonNull Activity activity) { | ||
| if (performanceEnabled) { |
There was a problem hiding this comment.
l: Why don't we do this in onActivityCreated?
There was a problem hiding this comment.
because that's when the FrameMetricsAggregator is able to start counting, before that, it can't do anything
sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java
Outdated
Show resolved
Hide resolved
| <provider | ||
| android:name=".SentryPerformanceProvider" | ||
| android:authorities="${applicationId}.SentryInitProvider" | ||
| android:initOrder="200" |
There was a problem hiding this comment.
| android:initOrder="200" | |
| android:initOrder="1" |
😄
There was a problem hiding this comment.
|
|
||
| private boolean isAllActivityCallbacksAvailable; | ||
|
|
||
| // does it need to be atomic? its only in the main thread |
There was a problem hiding this comment.
is it read by another thread? If no other thread uses it than we're probably fine
| import org.jetbrains.annotations.ApiStatus; | ||
|
|
||
| @ApiStatus.Internal | ||
| public final class MeasurementValue { |
There was a problem hiding this comment.
I wonder if we're better off without this class and using the float directly to avoid allocating these objects
There was a problem hiding this comment.
could be easily removed, its not a public API, but the idea was to be extendable like the API is, its an object with a single float property
|
Is this blocked? Is there anything I can do to help move this PR (particularly the App Startup bits) |
nop, this PR was the PoC, the PR for App startup is #1487 |
📜 Description
Just a POC
💡 Motivation and Context
💚 How did you test it?
📝 Checklist
🔮 Next steps