Skip to content

Commit

Permalink
feat(performance): report total frames, frame delay, slow & frozen fr…
Browse files Browse the repository at this point in the history
…ames (#2106)

* Add frame tracking option

* Current state

* Update

* Update

* Update

* Update

* Update

* Update Changelog

* Update

* update

* Format

* Update

* Remove _isFinished

* clean up

* format

* Clean up

* Add native channel to span frame collector

* Clean up

* Update

* Fix has scheduled frame

* Clean up

* Clean up

* Update

* Format

* Revert main.dart

* Docs

* Update docs and naming

* Improve naming

* Move adding performance collector to _initDefaultValues

* Implement fallback display refreshrate on Android using WindowManager.DefaultDisplay

* Calculate frame metrics in one loop and use SplayTreeSet for the active spans

* Use clock instead of getUtcDateTime

* Remove span directly

* Merge mocks from main

* Fix merge stuff

* increase test ranges

* Increase ranges and run format

* Remove unnecessary test

* improve test

* Fix ktlitn

* See if this fixes windows test

* see if this fixes

* log infos why it's failing

* Fix tests

* increase delay

* Fix test (at least on web)

* try with future.foreach

* try fix

* Fix

* fix

* fix

* fix?

* Remove enablesFrameTracking checking in onSpanFinished

* Fix macos refresh rate fetching

* Fix ktlint

* fix dart analyze

* Fix analyze

* Update implementation

* Break early out of metrics calculation due to sorted frames

* Update macos impl

* Use core graphics

* swift lint

* Add comment why we don't use CADisplayLink in macos
  • Loading branch information
buenaflor authored Jun 25, 2024
1 parent 7e7f0b1 commit acbd5d3
Show file tree
Hide file tree
Showing 18 changed files with 779 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

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

### Dependencies
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 @@ class SentrySpan extends ISentrySpan {
late final DateTime _startTimestamp;
final Hub _hub;

bool _isRootSpan = false;

bool get isRootSpan => _isRootSpan;

@internal
SentryTracer get tracer => _tracer;

final SentryTracer _tracer;

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

Expand All @@ -36,13 +43,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 @@ -56,17 +65,27 @@ class SentrySpan extends ISentrySpan {
}

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);
}
}

// 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 @@ 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
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 @@ 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 @@ -256,6 +263,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
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()
}
}

result.success(refreshRate)
}

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

Expand Down
62 changes: 62 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,64 @@ 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) {
// We don't use CADisplayLink for macOS because it's only available starting with macOS 14
guard let window = NSApplication.shared.keyWindow else {
result(nil)
return
}

guard let screen = window.screen else {
result(nil)
return
}

guard let displayID =
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID else {
result(nil)
return
}

guard let mode = CGDisplayCopyDisplayMode(displayID) else {
result(nil)
return
}

result(Int(mode.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 @@ 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 get hasScheduledFrame => WidgetsBinding.instance.hasScheduledFrame;
}
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 @@ class SentryNativeChannel
.map(DebugImage.fromJson)
.toList();
});

@override
Future<int?> displayRefreshRate() =>
_channel.invokeMethod('displayRefreshRate');
}
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

0 comments on commit acbd5d3

Please sign in to comment.