Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -59,6 +60,10 @@ class MainScaffold extends StatelessWidget {
child: const Text('Open another Scaffold'),
onPressed: () => SecondaryScaffold.openSecondaryScaffold(context),
),
RaisedButton(
child: const Text('Open Scaffold with slow drawing'),
onPressed: () => SlowScaffold.open(context),
),
RaisedButton(
child: const Text('Dart: try catch'),
onPressed: () => tryCatch(),
Expand Down Expand Up @@ -327,3 +332,44 @@ Future<void> makeWebRequest(BuildContext context) async {
},
);
}

class SlowScaffold extends StatelessWidget {
static Future<void> open(BuildContext context) {
return Navigator.push(
context,
MaterialPageRoute<void>(
settings:
const RouteSettings(name: 'SlowScaffold', arguments: 'sloooowwwww'),
builder: (context) {
return SlowScaffold();
},
),
);
}

@override
Widget build(BuildContext context) {
sleep(Duration(milliseconds: 20));
return Scaffold(
appBar: AppBar(
title: const Text('SecondaryScaffold'),
),
body: Center(
child: Column(
children: [
const Text(
'You have added a navigation event '
'to the crash reports breadcrumbs.',
),
MaterialButton(
child: const Text('Go back'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}
117 changes: 117 additions & 0 deletions flutter/lib/src/integrations/frame_timing_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'dart:async';
import 'dart:ui';

import 'package:flutter/widgets.dart';
import 'package:sentry/sentry.dart';

import '../sentry_flutter_options.dart';

/// Records the time which a frame takes to draw, if it's above
/// a certain threshold, i.e. [FrameTimingIntegration.badFrameThreshold].
///
/// Should not be added in debug mode because the performance of the debug mode
/// is not indicativ of the performance in release mode.
///
/// Remarks:
/// See [SchedulerBinding.addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html)
/// to learn more about the performance impact of using this.
///
/// Adding a `timingsCallback` has a real significant performance impact as
/// noted above. Thus this integration should only be added if it's enabled.
/// The enabled check should not happen inside the `timingsCallback`.
class FrameTimingIntegration extends Integration<SentryFlutterOptions> {
FrameTimingIntegration({
required this.reporter,
this.badFrameThreshold = const Duration(milliseconds: 16),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't yet found a way to determine the device current refresh rate.

});

final Duration badFrameThreshold;
final FrameTimingReporter reporter;

late Hub _hub;

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
// We don't need to call `WidgetsFlutterBinding.ensureInitialized()`
// because `WidgetsFlutterBindingIntegration` already calls it.
// If the instance is not created, we skip it to keep going.
final instance = WidgetsBinding.instance;
if (instance != null) {
_hub = hub;
instance.addTimingsCallback(_timingsCallback);
options.sdk.addIntegration('FrameTimingsIntegration');
} else {
options.logger(
SentryLevel.error,
'FrameTimingsIntegration failed to be installed',
);
}
}

void _timingsCallback(List<FrameTiming> timings) {
var count = 0;
var worstFrameDuration = Duration.zero;
for (final timing in timings) {
if (timing.totalSpan > badFrameThreshold) {
count = count + 1;
if (timing.totalSpan > worstFrameDuration) {
worstFrameDuration = timing.totalSpan;
}
}
}
if (count > 0) {
final totalDuration =
timings.map((e) => e.totalSpan).reduce((a, b) => a + b);

final message = _message(count, worstFrameDuration, totalDuration);

if (reporter == FrameTimingReporter.breadcrumb) {
_reportAsBreadcrumb(message);
} else {
// This callback does not allow async, so we captureMessage as
// a fire and forget action.
_reportAsEvent(message);
}
}
}

Future<void> _reportAsEvent(String message) {
return _hub.captureMessage(
message,
level: SentryLevel.warning,
);
}

void _reportAsBreadcrumb(String message) {
_hub.addBreadcrumb(Breadcrumb(
type: 'info',
category: 'ui',
message: message,
level: SentryLevel.warning,
));
}

String _message(
int badFrameCount,
Duration worstFrameDuration,
Duration totalDuration,
) {
return '$badFrameCount frames exceeded ${_formatMS(badFrameThreshold)} '
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Average frame time would be nice to know, too.

'in the last ${_formatMS(totalDuration)}. '
'The worst frame time in this time span was '
'${_formatMS(worstFrameDuration)}';
}

@override
FutureOr<void> close() {
WidgetsBinding.instance?.removeTimingsCallback(_timingsCallback);
}

/// Format milliseconds with more precision than absolut milliseconds
String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms';
}

enum FrameTimingReporter {
breadcrumb,
event,
}
1 change: 1 addition & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry/sentry.dart';

import 'flutter_enricher_event_processor.dart';
import 'integrations/frame_timing_integration.dart';
import 'sentry_flutter_options.dart';

import 'default_integrations.dart';
Expand Down