Skip to content

Proof of Concept: Frame Tracking #699

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

Closed
wants to merge 6 commits into from
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
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export 'src/sentry_attachment/sentry_attachment.dart';
export 'src/sentry_user_feedback.dart';
// tracing
export 'src/tracing.dart';
export 'src/sentry_measurement.dart';
18 changes: 18 additions & 0 deletions dart/lib/src/protocol/sentry_transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:meta/meta.dart';
import '../protocol.dart';
import '../sentry_tracer.dart';
import '../utils.dart';
import '../sentry_measurement.dart';

@immutable
class SentryTransaction extends SentryEvent {
Expand Down Expand Up @@ -30,6 +31,7 @@ class SentryTransaction extends SentryEvent {
SdkVersion? sdk,
SentryRequest? request,
String? type,
this.measurements,
}) : super(
eventId: eventId,
timestamp: timestamp ?? _tracer.endTimestamp,
Expand Down Expand Up @@ -70,13 +72,24 @@ class SentryTransaction extends SentryEvent {
json['start_timestamp'] =
formatDateAsIso8601WithMillisPrecision(startTimestamp);

final ms = measurements;
if (ms != null && ms.isNotEmpty) {
final map = <String, dynamic>{};
for (final m in ms) {
map[m.name] = m.toJson();
}
json['measurements'] = map;
}

return json;
}

bool get finished => timestamp != null;

bool get sampled => contexts.trace?.sampled == true;

final List<SentryMeasurement>? measurements;

@override
SentryTransaction copyWith({
SentryId? eventId,
Expand Down Expand Up @@ -105,6 +118,7 @@ class SentryTransaction extends SentryEvent {
List<SentryException>? exceptions,
List<SentryThread>? threads,
String? type,
List<SentryMeasurement>? measurements,
}) =>
SentryTransaction(
_tracer,
Expand All @@ -126,5 +140,9 @@ class SentryTransaction extends SentryEvent {
sdk: sdk ?? this.sdk,
request: request ?? this.request,
type: type ?? this.type,
measurements: (measurements != null
? List<SentryMeasurement>.from(measurements)
: null) ??
this.measurements,
);
}
53 changes: 53 additions & 0 deletions dart/lib/src/sentry_measurement.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class SentryMeasurement {
SentryMeasurement(this.name, this.value);

// Mobile / Desktop Vitals

/// Amount of frames drawn during a transaction
SentryMeasurement.totalFrames(this.value) : name = 'frames_total';

/// Amount of slow frames drawn during a transaction.
/// A slow frame is any frame longer than 1s / refreshrate.
/// So for example any frame slower than 16ms for a refresh rate of 60hz.
SentryMeasurement.slowFrames(this.value) : name = 'frames_slow';

/// Amount of frozen frames drawn during a transaction.
/// Typically defined as frames slower than 500ms.
SentryMeasurement.frozenFrames(this.value) : name = 'frames_frozen';

SentryMeasurement.coldAppStart(Duration duration)
: assert(!duration.isNegative),
name = 'app_start_cold',
value = duration.inMilliseconds;

SentryMeasurement.warmAppStart(Duration duration)
: assert(!duration.isNegative),
name = 'app_start_warm',
value = duration.inMilliseconds;

// Web Vitals

SentryMeasurement.firstContentfulPaint(Duration duration)
: assert(!duration.isNegative),
name = 'fcp',
value = duration.inMilliseconds;

SentryMeasurement.firstPaint(Duration duration)
: assert(!duration.isNegative),
name = 'fp',
value = duration.inMilliseconds;

SentryMeasurement.timeToFirstByte(Duration duration)
: assert(!duration.isNegative),
name = 'ttfb',
value = duration.inMilliseconds;

final String name;
final num value;

Map<String, dynamic> toJson() {
return <String, num>{
'value': value,
};
}
}
4 changes: 3 additions & 1 deletion dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class SentryTracer extends ISentrySpan {
}
});

final transaction = SentryTransaction(this);
final transaction = SentryTransaction(this, measurements: measurements);
await _hub.captureTransaction(transaction);
}
}
Expand Down Expand Up @@ -132,6 +132,8 @@ class SentryTracer extends ISentrySpan {
_rootSpan.setTag(key, value);
}

final List<SentryMeasurement> measurements = [];

@override
ISentrySpan startChild(
String operation, {
Expand Down
2 changes: 2 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
//var frameTracker = FrameTracker();
//frameTracker.start();
}

@override
Expand Down
70 changes: 70 additions & 0 deletions flutter/lib/src/navigation/frame_tracker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:ui';
import 'package:flutter/widgets.dart';
import 'package:sentry/sentry.dart';

class FrameTracker {
FrameTracker({
this.slowFrameThreshold = const Duration(milliseconds: 16),
this.frozenFrameThreshold = const Duration(milliseconds: 500),
required WidgetsBinding binding,
}) : _binding = binding;

final WidgetsBinding _binding;

SingletonFlutterWindow get window => _binding.window;

final Duration slowFrameThreshold;
final Duration frozenFrameThreshold;

DateTime? _timeStamp;

var _frameCount = 0;
var _slowCount = 0;
var _frozenCount = 0;

void _frameCallback(Duration _) {
final timeStamp = _timeStamp;
if (timeStamp == null) {
return;
}

// postFrameCallbacks are called just once,
// so we have to add it each frame.
_binding.addPostFrameCallback(_frameCallback);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe a ticker is a better candidate than this? See https://api.flutter.dev/flutter/scheduler/Ticker-class.html


final now = DateTime.now();
_timeStamp = now;
final duration = timeStamp.difference(now).abs();

if (duration > frozenFrameThreshold) {
_frozenCount++;
} else if (duration > slowFrameThreshold) {
_slowCount++;
}

_frameCount++;
}

void start() {
_timeStamp = DateTime.now();
_binding.addPostFrameCallback(_frameCallback);
}

List<SentryMeasurement> finish() {
final metrics = [
SentryMeasurement.totalFrames(_frameCount),
SentryMeasurement.frozenFrames(_frozenCount),
SentryMeasurement.slowFrames(_slowCount),
];

_reset();
return metrics;
}

void _reset() {
_timeStamp = null;
_frameCount = 0;
_slowCount = 0;
_frozenCount = 0;
}
}
14 changes: 13 additions & 1 deletion flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:sentry/sentry.dart';
// ignore: implementation_imports
import 'package:sentry/src/sentry_tracer.dart';
import '../../sentry_flutter.dart';
import 'frame_tracker.dart';

/// This key must be used so that the web interface displays the events nicely
/// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/
Expand Down Expand Up @@ -75,6 +77,8 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
final bool _setRouteNameAsTransaction;
final RouteNameExtractor? _routeNameExtractor;
final AdditionalInfoExtractor? _additionalInfoProvider;
final FrameTracker _frameTracker =
FrameTracker(binding: WidgetsBinding.instance!);

ISentrySpan? _transaction;

Expand Down Expand Up @@ -159,6 +163,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
autoFinishAfter: _autoFinishAfter,
trimEnd: true,
);
_frameTracker.start();
if (arguments != null) {
_transaction?.setData('route_settings_arguments', arguments);
}
Expand All @@ -170,6 +175,13 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {

Future<void> _finishTransaction() async {
_transaction?.status ??= SpanStatus.ok();
// ignore: invalid_use_of_internal_member
if (_transaction is SentryTracer) {
// ignore: invalid_use_of_internal_member
final transaction = _transaction as SentryTracer;
final measurements = _frameTracker.finish();
transaction.measurements.addAll(measurements);
}
return await _transaction?.finish();
}
}
Expand Down
2 changes: 1 addition & 1 deletion flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
package_info_plus: ^1.0.0

dev_dependencies:
build_runner: ^2.1.5
build_runner: ^2.1.0
flutter_test:
sdk: flutter
mockito: ^5.0.0
Expand Down