Skip to content
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(performance): report total frames, frame delay, slow & frozen frames #2106

Merged
merged 67 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
849df8e
Add frame tracking option
buenaflor Jun 11, 2024
633047b
Current state
buenaflor Jun 11, 2024
cf7c96c
Update
buenaflor Jun 11, 2024
cf6a130
Update
buenaflor Jun 13, 2024
cb8e94f
Update
buenaflor Jun 13, 2024
2219ec3
Update
buenaflor Jun 13, 2024
0880d0e
Update
buenaflor Jun 17, 2024
be90b2e
Merge remote-tracking branch 'origin/main' into feat/frame-duration
buenaflor Jun 17, 2024
60f62b6
Update Changelog
buenaflor Jun 17, 2024
7d22941
Update
buenaflor Jun 17, 2024
f116f40
update
buenaflor Jun 17, 2024
7d2c89f
Format
buenaflor Jun 18, 2024
da91065
Update
buenaflor Jun 18, 2024
01fa33d
Remove _isFinished
buenaflor Jun 18, 2024
140d2e1
clean up
buenaflor Jun 18, 2024
019feed
format
buenaflor Jun 18, 2024
38aa0d9
Clean up
buenaflor Jun 18, 2024
e88aa35
Add native channel to span frame collector
buenaflor Jun 18, 2024
a719da2
Clean up
buenaflor Jun 18, 2024
0e4d02c
Update
buenaflor Jun 18, 2024
90d5f2b
Fix has scheduled frame
buenaflor Jun 18, 2024
5ef1449
Clean up
buenaflor Jun 18, 2024
3ad6033
Clean up
buenaflor Jun 18, 2024
9aaaf4d
Update
buenaflor Jun 18, 2024
89f0e18
Format
buenaflor Jun 18, 2024
82cb1ce
Revert main.dart
buenaflor Jun 18, 2024
333dfd1
Docs
buenaflor Jun 18, 2024
c0d0c22
Update docs and naming
buenaflor Jun 20, 2024
0f60f13
Improve naming
buenaflor Jun 21, 2024
ab6b724
Move adding performance collector to _initDefaultValues
buenaflor Jun 21, 2024
c6628a3
Implement fallback display refreshrate on Android using WindowManager…
buenaflor Jun 21, 2024
d37891f
Calculate frame metrics in one loop and use SplayTreeSet for the acti…
buenaflor Jun 21, 2024
727f5ff
Use clock instead of getUtcDateTime
buenaflor Jun 21, 2024
ab53bd0
Remove span directly
buenaflor Jun 21, 2024
4081fe8
Merge mocks from main
buenaflor Jun 21, 2024
d27dd66
Merge branch 'main' into feat/frame-duration
buenaflor Jun 21, 2024
f62d241
Fix merge stuff
buenaflor Jun 21, 2024
c787e6a
increase test ranges
buenaflor Jun 21, 2024
c95bbed
Increase ranges and run format
buenaflor Jun 21, 2024
636adf4
Remove unnecessary test
buenaflor Jun 21, 2024
8218076
improve test
buenaflor Jun 21, 2024
f60c10c
Fix ktlitn
buenaflor Jun 21, 2024
3a7bd42
See if this fixes windows test
buenaflor Jun 21, 2024
98d118c
see if this fixes
buenaflor Jun 21, 2024
8a06d6a
log infos why it's failing
buenaflor Jun 21, 2024
33dc7c0
Fix tests
buenaflor Jun 22, 2024
cce13a0
increase delay
buenaflor Jun 22, 2024
1f159e3
Fix test (at least on web)
buenaflor Jun 22, 2024
3c7b714
try with future.foreach
buenaflor Jun 22, 2024
d6771ae
try fix
buenaflor Jun 22, 2024
f5a324c
Fix
buenaflor Jun 22, 2024
bd6046f
fix
buenaflor Jun 22, 2024
5782eb0
fix
buenaflor Jun 22, 2024
2dfb478
fix?
buenaflor Jun 22, 2024
b81faf5
Remove enablesFrameTracking checking in onSpanFinished
buenaflor Jun 24, 2024
9f14128
Fix macos refresh rate fetching
buenaflor Jun 24, 2024
1f4e58f
Fix ktlint
buenaflor Jun 24, 2024
1cecda2
fix dart analyze
buenaflor Jun 24, 2024
74a6d33
Fix analyze
buenaflor Jun 24, 2024
b5630cb
Merge branch 'main' into feat/frame-duration
buenaflor Jun 24, 2024
71cdcdb
Update implementation
buenaflor Jun 24, 2024
28de2ed
Break early out of metrics calculation due to sorted frames
buenaflor Jun 24, 2024
f7c73f0
Update macos impl
buenaflor Jun 25, 2024
061d5a0
Use core graphics
buenaflor Jun 25, 2024
9c4b19c
Merge branch 'main' into feat/frame-duration
buenaflor Jun 25, 2024
a8adfae
swift lint
buenaflor Jun 25, 2024
736bafc
Add comment why we don't use CADisplayLink in macos
buenaflor Jun 25, 2024
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Capture total frames, frame delay, slow & frozen frames and attach to spans ([#2106](https://github.com/getsentry/sentry-dart/pull/2106))

### Fixes

- Load contexts integration not setting `SentryUser` ([#2089](https://github.com/getsentry/sentry-dart/pull/2089))
Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export 'src/http_client/sentry_http_client_error.dart';
export 'src/sentry_attachment/sentry_attachment.dart';
export 'src/sentry_user_feedback.dart';
export 'src/utils/tracing_utils.dart';
export 'src/performance_collector.dart';
// tracing
export 'src/tracing.dart';
export 'src/hint.dart';
Expand Down
34 changes: 18 additions & 16 deletions dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -409,22 +409,24 @@ class Hub {
bool? trimEnd,
OnTransactionFinish? onFinish,
Map<String, dynamic>? customSamplingContext,
}) =>
startTransactionWithContext(
SentryTransactionContext(
name,
operation,
description: description,
origin: SentryTraceOrigins.manual,
),
startTimestamp: startTimestamp,
bindToScope: bindToScope,
waitForChildren: waitForChildren,
autoFinishAfter: autoFinishAfter,
trimEnd: trimEnd,
onFinish: onFinish,
customSamplingContext: customSamplingContext,
);
}) {
final tx = startTransactionWithContext(
SentryTransactionContext(
name,
operation,
description: description,
origin: SentryTraceOrigins.manual,
),
startTimestamp: startTimestamp,
bindToScope: bindToScope,
waitForChildren: waitForChildren,
autoFinishAfter: autoFinishAfter,
trimEnd: trimEnd,
onFinish: onFinish,
customSamplingContext: customSamplingContext,
);
return tx;
}

/// Creates a Transaction and returns the instance.
ISentrySpan startTransactionWithContext(
Expand Down
13 changes: 13 additions & 0 deletions dart/lib/src/performance_collector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import '../sentry.dart';

abstract class PerformanceCollector {}

/// Used for collecting continuous data about vitals (slow, frozen frames, etc.)
/// during a transaction/span.
abstract class PerformanceContinuousCollector extends PerformanceCollector {
void onSpanStarted(ISentrySpan span);

void onSpanFinished(ISentrySpan span, DateTime endTimestamp);

void clear();
}
26 changes: 21 additions & 5 deletions dart/lib/src/protocol/sentry_span.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import 'dart:async';

import '../hub.dart';
import '../../sentry.dart';
import '../metrics/local_metrics_aggregator.dart';
import '../protocol.dart';

import '../sentry_tracer.dart';
import '../tracing.dart';
import '../utils.dart';

typedef OnFinishedCallback = Future<void> Function({DateTime? endTimestamp});

Expand All @@ -16,6 +13,9 @@ class SentrySpan extends ISentrySpan {
Map<String, List<MetricSummary>>? _metricSummaries;
late final DateTime _startTimestamp;
final Hub _hub;
bool _isRootSpan = false;
buenaflor marked this conversation as resolved.
Show resolved Hide resolved

bool get isRootSpan => _isRootSpan;

final SentryTracer _tracer;
final Map<String, dynamic> _data = {};
Expand All @@ -36,13 +36,15 @@ class SentrySpan extends ISentrySpan {
DateTime? startTimestamp,
this.samplingDecision,
OnFinishedCallback? finishedCallback,
isRootSpan = false,
}) {
_startTimestamp = startTimestamp?.toUtc() ?? _hub.options.clock();
_finishedCallback = finishedCallback;
_origin = _context.origin;
_localMetricsAggregator = _hub.options.enableSpanLocalMetricAggregation
? LocalMetricsAggregator()
: null;
_isRootSpan = isRootSpan;
}

@override
Expand All @@ -67,12 +69,24 @@ class SentrySpan extends ISentrySpan {
_endTimestamp = endTimestamp.toUtc();
}

print('finishing: ${_context.description}');
// We need the timestamp so we can finish here
for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
final _timestamp = _endTimestamp;
if (_timestamp != null) {
collector.onSpanFinished(this, _timestamp);
}
}
}

// associate error
if (_throwable != null) {
_hub.setSpanContext(_throwable, this, _tracer.name);
}
_metricSummaries = _localMetricsAggregator?.getSummaries();
await _finishedCallback?.call(endTimestamp: _endTimestamp);
_finished = true;
return super.finish(status: status, endTimestamp: _endTimestamp);
}

Expand Down Expand Up @@ -197,8 +211,10 @@ class SentrySpan extends ISentrySpan {
return json;
}

bool _finished = false;

@override
bool get finished => _endTimestamp != null;
bool get finished => _finished;

@override
dynamic get throwable => _throwable;
Expand Down
8 changes: 8 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,14 @@ class SentryOptions {
return tracesSampleRate != null || tracesSampler != null;
}

List<PerformanceCollector> get performanceCollectors =>
_performanceCollectors;
final List<PerformanceCollector> _performanceCollectors = [];

void addPerformanceCollector(PerformanceCollector collector) {
_performanceCollectors.add(collector);
}

@internal
late SentryExceptionFactory exceptionFactory = SentryExceptionFactory(this);

Expand Down
17 changes: 16 additions & 1 deletion dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class SentryTracer extends ISentrySpan {
_hub,
samplingDecision: transactionContext.samplingDecision,
startTimestamp: startTimestamp,
isRootSpan: true,
);
_waitForChildren = waitForChildren;
_autoFinishAfter = autoFinishAfter;
Expand All @@ -80,6 +81,12 @@ class SentryTracer extends ISentrySpan {
SentryTransactionNameSource.custom;
_trimEnd = trimEnd;
_onFinish = onFinish;

for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
collector.onSpanStarted(_rootSpan);
}
}
}

@override
Expand Down Expand Up @@ -211,11 +218,13 @@ class SentryTracer extends ISentrySpan {
return NoOpSentrySpan();
}

return _rootSpan.startChild(
final child = _rootSpan.startChild(
operation,
description: description,
startTimestamp: startTimestamp,
);

return child;
}

ISentrySpan startChildWithParentSpanId(
Expand Down Expand Up @@ -256,6 +265,12 @@ class SentryTracer extends ISentrySpan {

_children.add(child);

for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
collector.onSpanStarted(child);
}
}

return child;
}

Expand Down
56 changes: 28 additions & 28 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@ final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
await setupSentry(
() => runApp(
SentryWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(),
child: const MyApp(),
),
),
),
exampleDsn,
);
() => runApp(
SentryWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(),
child: const MyApp(),
),
),
),
exampleDsn);
}

Future<void> setupSentry(
Expand Down Expand Up @@ -96,6 +95,7 @@ Future<void> setupSentry(
options.environment = 'integration';
options.beforeSend = beforeSendCallback;
}
options.beforeSend = beforeSendCallback;
},
// Init your App.
appRunner: appRunner,
Expand Down Expand Up @@ -420,24 +420,24 @@ class MainScaffold extends StatelessWidget {

await span.finish(status: const SpanStatus.resourceExhausted());

await Future.delayed(const Duration(milliseconds: 90));

final spanChild = span.startChild(
'childOfChildOfMyOp',
description: 'childOfChildOfMyOp span',
);

await Future.delayed(const Duration(milliseconds: 110));

spanChild.startChild(
'unfinishedChild',
description: 'I wont finish',
);

await spanChild.finish(
status: const SpanStatus.internalError());

await Future.delayed(const Duration(milliseconds: 50));
// await Future.delayed(const Duration(milliseconds: 90));
//
// final spanChild = span.startChild(
// 'childOfChildOfMyOp',
// description: 'childOfChildOfMyOp span',
// );
//
// await Future.delayed(const Duration(milliseconds: 110));
//
// spanChild.startChild(
// 'unfinishedChild',
// description: 'I wont finish',
// );
//
// await spanChild.finish(
// status: const SpanStatus.internalError());
//
// await Future.delayed(const Duration(milliseconds: 50));
// findPrimeNumber(1000000); // Uncomment to see it with profiling
await transaction.finish(status: const SpanStatus.ok());
},
Expand Down
21 changes: 21 additions & 0 deletions flutter/lib/src/frame_callback_handler.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';

abstract class FrameCallbackHandler {
void addPostFrameCallback(FrameCallback callback);
void addPersistentFrameCallback(FrameCallback callback);
Future<void> get endOfFrame;
late bool hasScheduledFrame;
}

class DefaultFrameCallbackHandler implements FrameCallbackHandler {
Expand All @@ -12,4 +16,21 @@ class DefaultFrameCallbackHandler implements FrameCallbackHandler {
SchedulerBinding.instance.addPostFrameCallback(callback);
} catch (_) {}
}

@override
void addPersistentFrameCallback(FrameCallback callback) {
try {
WidgetsBinding.instance.addPersistentFrameCallback(callback);
} catch (_) {}
}

@override
Future<void> get endOfFrame async {
try {
await WidgetsBinding.instance.endOfFrame;
} catch (_) {}
}

@override
bool hasScheduledFrame = true;
}
4 changes: 4 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'span_frame_metrics_collector.dart';
import '../sentry_flutter.dart';
import 'event_processor/android_platform_exception_event_processor.dart';
import 'event_processor/flutter_exception_event_processor.dart';
Expand Down Expand Up @@ -90,6 +91,9 @@ mixin SentryFlutter {

await _initDefaultValues(flutterOptions, channel);

flutterOptions
.addPerformanceCollector(SpanFrameMetricsCollector(flutterOptions));
buenaflor marked this conversation as resolved.
Show resolved Hide resolved

await Sentry.init(
(options) => optionsConfiguration(options as SentryFlutterOptions),
appRunner: appRunner,
Expand Down
14 changes: 14 additions & 0 deletions flutter/lib/src/sentry_flutter_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,20 @@ class SentryFlutterOptions extends SentryOptions {
/// Read timeout. This will only be synced to the Android native SDK.
Duration readTimeout = Duration(seconds: 5);

/// Enable or disable Frames Tracking, which is used to report frame information
/// for every [ISentrySpan].
///
/// When enabled, the following metrics are reported for each span:
/// - Slow frames: The number of frames that exceeded a specified threshold for frame duration.
/// - Frozen frames: The number of frames that took an unusually long time to render, indicating a potential freeze or hang.
/// - Total frames count: The total number of frames rendered during the span.
/// - Frames delay: The delayed frame render duration of all frames.

/// Read more about frames tracking here: https://develop.sentry.dev/sdk/performance/frames-delay/
///
/// Defaults to `true`
bool enableFramesTracking = true;

/// By using this, you are disabling native [Breadcrumb] tracking and instead
/// you are just tracking [Breadcrumb]s which result from events available
/// in the current Flutter environment.
Expand Down
Loading
Loading