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 60 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Features

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

- Bump Cocoa SDK from v8.29.0 to v8.29.1 ([#2109](https://github.com/getsentry/sentry-dart/pull/2109))
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
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 {
Future<void> onSpanStarted(ISentrySpan span);

Future<void> onSpanFinished(ISentrySpan span, DateTime endTimestamp);

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

import '../hub.dart';
import 'package:meta/meta.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 @@ -17,7 +16,15 @@
late final DateTime _startTimestamp;
final Hub _hub;

bool _isRootSpan = false;
buenaflor marked this conversation as resolved.
Show resolved Hide resolved

bool get isRootSpan => _isRootSpan;

Check warning on line 21 in dart/lib/src/protocol/sentry_span.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_span.dart#L21

Added line #L21 was not covered by tests

@internal
SentryTracer get tracer => _tracer;

Check warning on line 24 in dart/lib/src/protocol/sentry_span.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_span.dart#L23-L24

Added lines #L23 - L24 were not covered by tests

final SentryTracer _tracer;

final Map<String, dynamic> _data = {};
dynamic _throwable;

Expand All @@ -36,13 +43,15 @@
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 @@ -56,17 +65,27 @@
}

if (endTimestamp == null) {
_endTimestamp = _hub.options.clock();
endTimestamp = _hub.options.clock();
} else if (endTimestamp.isBefore(_startTimestamp)) {
_hub.options.logger(
SentryLevel.warning,
'End timestamp ($endTimestamp) cannot be before start timestamp ($_startTimestamp)',
);
_endTimestamp = _hub.options.clock();
endTimestamp = _hub.options.clock();
} else {
_endTimestamp = endTimestamp.toUtc();
endTimestamp = endTimestamp.toUtc();
}

for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
await collector.onSpanFinished(this, endTimestamp);

Check warning on line 81 in dart/lib/src/protocol/sentry_span.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_span.dart#L80-L81

Added lines #L80 - L81 were not covered by tests
}
}

// The finished flag depends on the _endTimestamp
// If we set this earlier then finished is true and then we cannot use setData etc...
_endTimestamp = endTimestamp;

// associate error
if (_throwable != null) {
_hub.setSpanContext(_throwable, this, _tracer.name);
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 @@
return tracesSampleRate != null || tracesSampler != null;
}

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

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

Check warning on line 509 in dart/lib/src/sentry_options.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/sentry_options.dart#L508-L509

Added lines #L508 - L509 were not covered by tests
}

@internal
late SentryExceptionFactory exceptionFactory = SentryExceptionFactory(this);

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

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

Check warning on line 87 in dart/lib/src/sentry_tracer.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/sentry_tracer.dart#L86-L87

Added lines #L86 - L87 were not covered by tests
}
}
}

@override
Expand Down Expand Up @@ -256,6 +263,12 @@

_children.add(child);

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

Check warning on line 268 in dart/lib/src/sentry_tracer.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/sentry_tracer.dart#L267-L268

Added lines #L267 - L268 were not covered by tests
}
}

return child;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.sentry.flutter

import android.app.Activity
import android.content.Context
import android.os.Build
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
Expand Down Expand Up @@ -72,6 +73,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
"removeTag" -> removeTag(call.argument("key"), result)
"loadContexts" -> loadContexts(result)
"displayRefreshRate" -> displayRefreshRate(result)
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -179,6 +181,29 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}

private fun displayRefreshRate(result: Result) {
var refreshRate: Int? = null

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val display = activity?.get()?.display
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
} else {
val display =
activity
?.get()
?.window
?.windowManager
?.defaultDisplay
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
}
buenaflor marked this conversation as resolved.
Show resolved Hide resolved

result.success(refreshRate)
}

private fun TimeSpan.addToMap(map: MutableMap<String, Any?>) {
if (startTimestamp == null) return

Expand Down
56 changes: 56 additions & 0 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import UIKit
#elseif os(macOS)
import FlutterMacOS
import AppKit
import CoreVideo
#endif

// swiftlint:disable file_length function_body_length
Expand Down Expand Up @@ -164,6 +165,9 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
collectProfile(call, result)
#endif

case "displayRefreshRate":
displayRefreshRate(result)

default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -651,6 +655,58 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
PrivateSentrySDKOnly.discardProfiler(forTrace: SentryId(uuidString: traceId))
result(nil)
}

#if os(iOS)
// Taken from the Flutter engine:
// https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L150
private func displayRefreshRate(_ result: @escaping FlutterResult) {
let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
displayLink.isPaused = true

let preferredFPS = displayLink.preferredFramesPerSecond
displayLink.invalidate()

if preferredFPS != 0 {
result(preferredFPS)
return
}

if #available(iOS 13.0, *) {
guard let windowScene = UIApplication.shared.windows.first?.windowScene else {
result(nil)
return
}
result(windowScene.screen.maximumFramesPerSecond)
} else {
result(UIScreen.main.maximumFramesPerSecond)
}
}

@objc private func onDisplayLink(_ displayLink: CADisplayLink) {
// No-op
}
#elseif os(macOS)
private func displayRefreshRate(_ result: @escaping FlutterResult) {
let displayID: CGDirectDisplayID = CGMainDisplayID()
var displayLink: CVDisplayLink?

if CVDisplayLinkCreateWithCGDisplay(displayID, &displayLink) != kCVReturnSuccess {
result(nil)
return
}

guard let link = displayLink else {
result(nil)
return
}

let period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link)
let refreshRate = Int(round(Double(period.timeScale) / Double(period.timeValue)))

result(refreshRate)
}
#endif
}

// swiftlint:enable function_body_length
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;
bool get hasScheduledFrame;
}

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

@override

Check warning on line 20 in flutter/lib/src/frame_callback_handler.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/frame_callback_handler.dart#L20

Added line #L20 was not covered by tests
void addPersistentFrameCallback(FrameCallback callback) {
try {
WidgetsBinding.instance.addPersistentFrameCallback(callback);

Check warning on line 23 in flutter/lib/src/frame_callback_handler.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/frame_callback_handler.dart#L23

Added line #L23 was not covered by tests
} catch (_) {}
}

@override

Check warning on line 27 in flutter/lib/src/frame_callback_handler.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/frame_callback_handler.dart#L27

Added line #L27 was not covered by tests
Future<void> get endOfFrame async {
try {
await WidgetsBinding.instance.endOfFrame;

Check warning on line 30 in flutter/lib/src/frame_callback_handler.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/frame_callback_handler.dart#L30

Added line #L30 was not covered by tests
} catch (_) {}
}

@override
bool get hasScheduledFrame => WidgetsBinding.instance.hasScheduledFrame;

Check warning on line 35 in flutter/lib/src/frame_callback_handler.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/frame_callback_handler.dart#L34-L35

Added lines #L34 - L35 were not covered by tests
}
2 changes: 2 additions & 0 deletions flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ abstract class SentryNativeBinding {

Future<void> discardProfiler(SentryId traceId);

Future<int?> displayRefreshRate();

Future<Map<String, dynamic>?> collectProfile(
SentryId traceId, int startTimeNs, int endTimeNs);

Expand Down
4 changes: 4 additions & 0 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,8 @@
.map(DebugImage.fromJson)
.toList();
});

@override

Check warning on line 182 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L182

Added line #L182 was not covered by tests
Future<int?> displayRefreshRate() =>
_channel.invokeMethod('displayRefreshRate');

Check warning on line 184 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L184

Added line #L184 was not covered by tests
}
3 changes: 3 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:ui';

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 @@ -135,6 +136,8 @@ mixin SentryFlutter {

options.addEventProcessor(PlatformExceptionEventProcessor());

options.addPerformanceCollector(SpanFrameMetricsCollector(options));

_setSdk(options);
}

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