Skip to content

Commit 07cd9e8

Browse files
authored
feat: use addTimingsCallback instead of addPostFrameCallback to determine app start end (#2405)
* use addTimingsCallback * update test * update * update naming * update naming * update changelog * fix test * check if using custom timingscallback works
1 parent 404f5a9 commit 07cd9e8

11 files changed

+199
-137
lines changed

CHANGELOG.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
# Changelog
22

33
## Unreleased
4-
5-
### Enhancements
6-
7-
- Only send debug images referenced in the stacktrace for events ([#2329](https://github.com/getsentry/sentry-dart/pull/2329))
8-
- Remove `sentry` frames if SDK falls back to current stack trace ([#2351](https://github.com/getsentry/sentry-dart/pull/2351))
9-
- Flutter doesn't always provide stack traces for unhandled errors - this is normal Flutter behavior
10-
- When no stack trace is provided (in Flutter errors, `captureException`, or `captureMessage`):
11-
- SDK creates a synthetic trace using `StackTrace.current`
12-
- Internal SDK frames are removed to reduce noise
13-
- Original stack traces (when provided) are left unchanged
144

155
### Features
166

7+
- Improve app start measurements by using `addTimingsCallback` instead of `addPostFrameCallback` to determine app start end ([#2405](https://github.com/getsentry/sentry-dart/pull/2405))
8+
- ⚠️ This change may result in reporting of shorter app start durations
179
- Improve frame tracking accuracy ([#2372](https://github.com/getsentry/sentry-dart/pull/2372))
1810
- Introduces `SentryWidgetsFlutterBinding` that tracks a frame starting from `handleBeginFrame` and ending in `handleDrawFrame`, this is approximately the [buildDuration](https://api.flutter.dev/flutter/dart-ui/FrameTiming/buildDuration.html) time
1911
- By default, `SentryFlutter.init()` automatically initializes `SentryWidgetsFlutterBinding` through the `WidgetsFlutterBindingIntegration`
@@ -29,6 +21,16 @@
2921
```
3022
- ⚠️ Frame tracking will be disabled if a different binding is used
3123

24+
### Enhancements
25+
26+
- Only send debug images referenced in the stacktrace for events ([#2329](https://github.com/getsentry/sentry-dart/pull/2329))
27+
- Remove `sentry` frames if SDK falls back to current stack trace ([#2351](https://github.com/getsentry/sentry-dart/pull/2351))
28+
- Flutter doesn't always provide stack traces for unhandled errors - this is normal Flutter behavior
29+
- When no stack trace is provided (in Flutter errors, `captureException`, or `captureMessage`):
30+
- SDK creates a synthetic trace using `StackTrace.current`
31+
- Internal SDK frames are removed to reduce noise
32+
- Original stack traces (when provided) are left unchanged
33+
3234
### Fixes
3335

3436
- Apply default IP address (`{{auto}}`) to transactions ([#2395](https://github.com/getsentry/sentry-dart/pull/2395))
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import 'package:flutter/cupertino.dart';
22
import 'package:flutter/scheduler.dart';
33

4+
/// Use instead of TimingsCallback as it is not available in the Flutter min version
5+
typedef SentryTimingsCallback = void Function(List<FrameTiming> timings);
6+
47
abstract class FrameCallbackHandler {
58
void addPostFrameCallback(FrameCallback callback);
6-
void addPersistentFrameCallback(FrameCallback callback);
7-
Future<void> get endOfFrame;
8-
bool get hasScheduledFrame;
9+
void removeTimingsCallback(SentryTimingsCallback callback);
10+
void addTimingsCallback(SentryTimingsCallback callback);
911
}
1012

1113
class DefaultFrameCallbackHandler implements FrameCallbackHandler {
@@ -18,19 +20,16 @@ class DefaultFrameCallbackHandler implements FrameCallbackHandler {
1820
}
1921

2022
@override
21-
void addPersistentFrameCallback(FrameCallback callback) {
23+
void addTimingsCallback(SentryTimingsCallback callback) {
2224
try {
23-
WidgetsBinding.instance.addPersistentFrameCallback(callback);
25+
WidgetsBinding.instance.addTimingsCallback(callback);
2426
} catch (_) {}
2527
}
2628

2729
@override
28-
Future<void> get endOfFrame async {
30+
void removeTimingsCallback(SentryTimingsCallback callback) {
2931
try {
30-
await WidgetsBinding.instance.endOfFrame;
32+
WidgetsBinding.instance.removeTimingsCallback(callback);
3133
} catch (_) {}
3234
}
33-
34-
@override
35-
bool get hasScheduledFrame => WidgetsBinding.instance.hasScheduledFrame;
3635
}

flutter/lib/src/integrations/native_app_start_integration.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:ui';
23

34
import 'package:meta/meta.dart';
45

@@ -28,15 +29,24 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
2829
}
2930

3031
final Completer<void> _appStartEndCompleter = Completer<void>();
32+
bool _allowProcessing = true;
3133

3234
@override
3335
void call(Hub hub, SentryFlutterOptions options) async {
34-
_frameCallbackHandler.addPostFrameCallback((timeStamp) async {
36+
void timingsCallback(List<FrameTiming> timings) async {
37+
if (!_allowProcessing) {
38+
return;
39+
}
40+
// Set immediately to prevent multiple executions
41+
// we only care about the first frame
42+
_allowProcessing = false;
43+
3544
try {
3645
DateTime? appStartEnd;
3746
if (options.autoAppStart) {
3847
// ignore: invalid_use_of_internal_member
39-
appStartEnd = options.clock();
48+
appStartEnd = DateTime.fromMicrosecondsSinceEpoch(timings.first
49+
.timestampInMicroseconds(FramePhase.rasterFinishWallTime));
4050
} else if (_appStartEnd == null) {
4151
await _appStartEndCompleter.future.timeout(
4252
const Duration(seconds: 10),
@@ -62,8 +72,12 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
6272
if (options.automatedTestMode) {
6373
rethrow;
6474
}
75+
} finally {
76+
_frameCallbackHandler.removeTimingsCallback(timingsCallback);
6577
}
66-
});
78+
}
79+
80+
_frameCallbackHandler.addTimingsCallback(timingsCallback);
6781
options.sdk.addIntegration('nativeAppStartIntegration');
6882
}
6983
}

flutter/lib/src/sentry_flutter.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ mixin SentryFlutter {
171171
}
172172
integrations.add(LoadImageListIntegration(native));
173173
integrations.add(FramesTrackingIntegration(native));
174+
integrations.add(
175+
NativeAppStartIntegration(
176+
DefaultFrameCallbackHandler(),
177+
NativeAppStartHandler(native),
178+
),
179+
);
174180
options.enableDartSymbolication = false;
175181
}
176182

@@ -193,14 +199,6 @@ mixin SentryFlutter {
193199
// in errors.
194200
integrations.add(LoadReleaseIntegration());
195201

196-
if (native != null) {
197-
integrations.add(
198-
NativeAppStartIntegration(
199-
DefaultFrameCallbackHandler(),
200-
NativeAppStartHandler(native),
201-
),
202-
);
203-
}
204202
return integrations;
205203
}
206204

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import 'package:flutter/scheduler.dart';
22
import 'package:sentry_flutter/src/frame_callback_handler.dart';
33

4-
import 'mocks.dart';
5-
64
class FakeFrameCallbackHandler implements FrameCallbackHandler {
7-
final Duration finishAfterDuration;
5+
FakeFrameCallbackHandler({this.postFrameCallbackDelay});
86

9-
FakeFrameCallbackHandler(
10-
{this.finishAfterDuration = const Duration(milliseconds: 50)});
7+
/// If set, it automatically executes the callback after the delay
8+
Duration? postFrameCallbackDelay;
9+
FrameCallback? postFrameCallback;
10+
TimingsCallback? timingsCallback;
1111

1212
@override
1313
void addPostFrameCallback(FrameCallback callback) async {
14-
// ignore: inference_failure_on_instance_creation
15-
await Future.delayed(finishAfterDuration);
16-
callback(Duration.zero);
17-
}
14+
postFrameCallback = callback;
1815

19-
@override
20-
Future<void> addPersistentFrameCallback(FrameCallback callback) async {
21-
for (final duration in fakeFrameDurations) {
22-
// Let's wait a bit so the timestamp intervals are large enough
23-
await Future<void>.delayed(Duration(milliseconds: 20));
24-
callback(duration);
16+
if (postFrameCallbackDelay != null) {
17+
await Future<void>.delayed(postFrameCallbackDelay!);
18+
callback(Duration.zero);
2519
}
2620
}
2721

2822
@override
29-
bool hasScheduledFrame = true;
23+
void addTimingsCallback(TimingsCallback callback) {
24+
timingsCallback = callback;
25+
}
3026

3127
@override
32-
Future<void> get endOfFrame => Future<void>.value();
28+
void removeTimingsCallback(TimingsCallback callback) {
29+
if (timingsCallback == callback) {
30+
timingsCallback = null;
31+
}
32+
}
3333
}

0 commit comments

Comments
 (0)