-
-
Notifications
You must be signed in to change notification settings - Fork 236
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
feat: improve frame tracking accuracy #2372
base: main
Are you sure you want to change the base?
Conversation
|
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2372 +/- ##
==========================================
+ Coverage 84.78% 85.07% +0.29%
==========================================
Files 253 256 +3
Lines 9070 9127 +57
==========================================
+ Hits 7690 7765 +75
+ Misses 1380 1362 -18 ☔ View full report in Codecov by Sentry. |
Android Performance metrics 🚀
|
Revision | Plain | With Sentry | Diff |
---|---|---|---|
1a93825 | 347.31 ms | 424.54 ms | 77.23 ms |
a510d1d | 295.63 ms | 344.65 ms | 49.03 ms |
a7acb24 | 301.00 ms | 357.38 ms | 56.38 ms |
6034b0a | 406.35 ms | 475.93 ms | 69.59 ms |
2d74010 | 400.42 ms | 466.50 ms | 66.08 ms |
4b5a4f6 | 394.19 ms | 471.81 ms | 77.62 ms |
bf8d36c | 505.00 ms | 563.18 ms | 58.18 ms |
e82709a | 361.18 ms | 423.50 ms | 62.32 ms |
c732386 | 316.84 ms | 390.62 ms | 73.78 ms |
24f71aa | 358.49 ms | 455.90 ms | 97.41 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
1a93825 | 6.27 MiB | 7.20 MiB | 956.36 KiB |
a510d1d | 5.94 MiB | 6.96 MiB | 1.02 MiB |
a7acb24 | 5.94 MiB | 6.95 MiB | 1.01 MiB |
6034b0a | 6.34 MiB | 7.29 MiB | 970.36 KiB |
2d74010 | 6.33 MiB | 7.26 MiB | 943.41 KiB |
4b5a4f6 | 6.34 MiB | 7.28 MiB | 962.57 KiB |
bf8d36c | 6.49 MiB | 7.56 MiB | 1.07 MiB |
e82709a | 6.34 MiB | 7.29 MiB | 970.37 KiB |
c732386 | 6.26 MiB | 7.20 MiB | 958.78 KiB |
24f71aa | 6.06 MiB | 7.03 MiB | 990.30 KiB |
Previous results on branch: feat/improve-frame-tracking
Startup times
Revision | Plain | With Sentry | Diff |
---|---|---|---|
20670eb | 507.39 ms | 559.13 ms | 51.74 ms |
49a132c | 463.74 ms | 528.69 ms | 64.95 ms |
8ccee38 | 516.76 ms | 581.57 ms | 64.82 ms |
533d18f | 477.00 ms | 525.20 ms | 48.20 ms |
2a85afd | 593.73 ms | 649.40 ms | 55.66 ms |
b738cab | 484.94 ms | 529.06 ms | 44.13 ms |
c47b0ca | 609.23 ms | 652.24 ms | 43.02 ms |
4b3335e | 452.90 ms | 500.60 ms | 47.70 ms |
f65fc08 | 441.52 ms | 492.14 ms | 50.62 ms |
54e4e7c | 451.19 ms | 506.00 ms | 54.81 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
20670eb | 6.49 MiB | 7.57 MiB | 1.08 MiB |
49a132c | 6.49 MiB | 7.56 MiB | 1.07 MiB |
8ccee38 | 6.49 MiB | 7.57 MiB | 1.08 MiB |
533d18f | 6.49 MiB | 7.57 MiB | 1.08 MiB |
2a85afd | 6.49 MiB | 7.56 MiB | 1.07 MiB |
b738cab | 6.49 MiB | 7.56 MiB | 1.07 MiB |
c47b0ca | 6.49 MiB | 7.57 MiB | 1.08 MiB |
4b3335e | 6.49 MiB | 7.56 MiB | 1.07 MiB |
f65fc08 | 6.49 MiB | 7.56 MiB | 1.07 MiB |
54e4e7c | 6.49 MiB | 7.56 MiB | 1.07 MiB |
iOS Performance metrics 🚀
|
Revision | Plain | With Sentry | Diff |
---|---|---|---|
d4d0807 | 1246.94 ms | 1260.69 ms | 13.75 ms |
f9d18f3 | 1240.20 ms | 1242.78 ms | 2.57 ms |
04bd9e6 | 1230.78 ms | 1250.71 ms | 19.94 ms |
051e97a | 1245.94 ms | 1249.51 ms | 3.57 ms |
b8562d0 | 1249.92 ms | 1267.56 ms | 17.64 ms |
affcf07 | 1240.61 ms | 1266.49 ms | 25.88 ms |
2e8b1e1 | 1247.45 ms | 1263.67 ms | 16.22 ms |
0f067d3 | 1245.71 ms | 1269.59 ms | 23.88 ms |
f1314d5 | 1246.46 ms | 1270.92 ms | 24.46 ms |
e239c83 | 1248.40 ms | 1269.28 ms | 20.89 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
d4d0807 | 8.33 MiB | 9.64 MiB | 1.31 MiB |
f9d18f3 | 8.29 MiB | 9.36 MiB | 1.07 MiB |
04bd9e6 | 8.33 MiB | 9.61 MiB | 1.27 MiB |
051e97a | 8.28 MiB | 9.34 MiB | 1.06 MiB |
b8562d0 | 8.33 MiB | 9.54 MiB | 1.22 MiB |
affcf07 | 8.38 MiB | 9.70 MiB | 1.33 MiB |
2e8b1e1 | 8.33 MiB | 9.64 MiB | 1.31 MiB |
0f067d3 | 8.32 MiB | 9.52 MiB | 1.20 MiB |
f1314d5 | 8.10 MiB | 9.08 MiB | 1004.30 KiB |
e239c83 | 8.38 MiB | 9.74 MiB | 1.36 MiB |
Previous results on branch: feat/improve-frame-tracking
Startup times
Revision | Plain | With Sentry | Diff |
---|---|---|---|
4b3335e | 1254.71 ms | 1269.00 ms | 14.29 ms |
533d18f | 1241.61 ms | 1260.67 ms | 19.05 ms |
ef339ab | 1249.04 ms | 1270.83 ms | 21.79 ms |
f65fc08 | 1239.18 ms | 1255.53 ms | 16.35 ms |
54e4e7c | 1257.43 ms | 1277.96 ms | 20.53 ms |
49a132c | 1247.24 ms | 1275.36 ms | 28.11 ms |
f1fa382 | 1244.14 ms | 1253.31 ms | 9.16 ms |
3be83c6 | 1255.57 ms | 1279.31 ms | 23.73 ms |
20670eb | 1248.35 ms | 1271.23 ms | 22.88 ms |
c47b0ca | 1232.20 ms | 1246.88 ms | 14.67 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
4b3335e | 8.38 MiB | 9.74 MiB | 1.36 MiB |
533d18f | 8.38 MiB | 9.74 MiB | 1.37 MiB |
ef339ab | 8.38 MiB | 9.74 MiB | 1.36 MiB |
f65fc08 | 8.38 MiB | 9.74 MiB | 1.36 MiB |
54e4e7c | 8.38 MiB | 9.74 MiB | 1.36 MiB |
49a132c | 8.38 MiB | 9.74 MiB | 1.36 MiB |
f1fa382 | 8.38 MiB | 9.74 MiB | 1.37 MiB |
3be83c6 | 8.38 MiB | 9.74 MiB | 1.37 MiB |
20670eb | 8.38 MiB | 9.74 MiB | 1.37 MiB |
c47b0ca | 8.38 MiB | 9.75 MiB | 1.37 MiB |
flutter/lib/src/binding_wrapper.dart
Outdated
} | ||
|
||
WidgetsBinding? _ambiguate(WidgetsBinding? binding) => binding; | ||
|
||
mixin SentryFrameTrackingBindingMixin on WidgetsBinding { |
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.
Fun Fact: If you end up shipping your own WidgetsBinding
, you could add performance tracing for MethodChannels as auto instrumentation by overriding one or both of the following properties:
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.
Ah nice, good to know :D sounds like something we can definitely do
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.
The defaults are not used by all plugins, but by the majority of them. If I remember correctly a notable exception are Flutter plugins that use pigeon
to generate the whole MethodChannel communication code.
@stefanosiano ptal when you have time 🙏 |
import 'package:meta/meta.dart'; | ||
import '../../sentry_flutter.dart'; | ||
|
||
/// 30s span duration at 120fps = 3600 frames |
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.
Can we always assume 120fps? Wouldn't it take double the time at 60 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.
in reality this is an arbitrary number it doesn't need to be 3600 but it's imo a good starting point
- realistically it won't be reached anyway because we only keep slow or frozen frames (delayed frames) in memory
the comment about fps should be changed because it doesn't really make sense here since we only store delayed frames
spanStartTimestamp: startTimestamp, spanEndTimestamp: endTimestamp); | ||
metrics?.applyTo(span); | ||
|
||
activeSpans.remove(span); |
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 keep all frames in memory now, until all spans finish
instead, we can remove older frames when we finish a span, like we did before
exceededFrames.removeWhere((frameTimestamp, _) =>
frameTimestamp.isBefore(activeSpans.first.startTimestamp));
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'm not sure whether adding this is wasting cpu power since this line removes frames that are the oldest / don't contribute anymore to any of the spans but since we only start frames tracking when we start and stop transactions there is no frame that can be removed.
for example stopping a child span would try to remove older frames but there should be none older than the root span / transaction
or maybe my thinking here is wrong?
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's true, until we run two (or more) transactions concurrently, right?
you could save the oldest frame timestamp, so that you can do just one comparison before deleting the frames
Something like
if (finishedSpan.startDate > firstFrameStartTimestamp) {
_delayedFrames.removeWhere((frame) => frame.startTimestamp.isBefore(spanStartTimestamp));
firstFrameStartTimestamp = _delayedFrames.firstOrNull?.startTimestamp
}
wdyt?
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.
oh yeah sry didn't think of concurrent ones, your suggestion is good 👍
widgetsBinding is! SentryWidgetsBindingMixin) { | ||
return abortInitWith( | ||
reason: | ||
'incompatible binding, SentryFlutterWidgetsBinding has not been instantiated'); |
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 would add the instruction in the log message (Please, use SentryFlutterWidgetsBinding.ensureInitialized()
instead of FlutterWidgetsBinding.ensureInitialized()
)
And I'd make it a warning
, or at least info
level
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.
just add a cleanup of the frames on span finish
Great!
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.
Just some general comments, suggestions and questions.
} | ||
|
||
bool _isFrameValid(DateTime? startTimestamp, DateTime? endTimestamp) { | ||
return startTimestamp != null && endTimestamp != 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.
Should we also check of startTimestamp < endTimestamp here?
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.
Also, endTimestamp from options.clock() will always be non-null. No need to have an optional param here and do the null check.
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.
Should we also check of startTimestamp < endTimestamp here?
we should, yeah
int frozenFramesDuration = 0; | ||
int framesDelay = 0; | ||
|
||
for (final timing in delayedFrames) { |
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.
Do we even need to iterate all delayed frames when we do have the relevantFrames
from before for the span star/end? If not, we could search for the first relevant frame above and bail out early from the loop if we are only interested if there are
relevant frames. Would be faster that way.
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.
Do we even need to iterate all delayed frames when we do have the relevantFrames from before for the span star/end? If not
nope, good catch, this should be iterating the relevantFrames not all frames
for (final timing in delayedFrames) { | ||
final frameStartMs = timing.startTimestamp.millisecondsSinceEpoch; | ||
final frameEndMs = timing.endTimestamp.millisecondsSinceEpoch; | ||
final actualDurationMs = timing.duration.inMilliseconds; |
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.
Nitpick: Could call this frameDurationMs
. It's easier to read (for me), as it relates to frameStarMs
and frameEndMs
Future<void> call(Hub hub, SentryFlutterOptions options) async { | ||
_options = options; | ||
|
||
void abortInitWith({required String reason}) { |
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: we could rename the method and required parameter. It's only called within this method and would make the calls a bit more readable. Also it's only a logging helper, so the function name could reflect that.
abortInitWith(reason: "foo")
vs
log("bar")
flutter/lib/src/sentry_flutter.dart
Outdated
@@ -155,6 +148,11 @@ mixin SentryFlutter { | |||
// Will call WidgetsFlutterBinding.ensureInitialized() before all other integrations. | |||
integrations.add(WidgetsFlutterBindingIntegration()); | |||
|
|||
options.logger( |
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.
Is this something we always want to print, or only when some condition is met?
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.
phew, not sure how that landed in there
this is not part of my PR
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'll remove it
📜 Description
Changes the frames collected from
addPersistentFrameCallback
to using a custom widgets flutter binding where we overridehandleBeginFrame
andhandleDrawFrame
. We are targeting thebuildDuration
If the user already initialized the default or another binding before
SentryFlutter.init
then we simply don't collect any frames. Otherwise we urge the user to useSentryWidgetsFlutterBinding.ensureInitialized()
if they need the binding before Sentry is initialized💡 Motivation and Context
Flutter devtool results
Our results
It's quite accurate and very close to what Flutter reports from the engine. (the build duration, not the raster)
💚 How did you test it?
Unit test, testing with widget, manual test
📝 Checklist
sendDefaultPii
is enabled🔮 Next steps