From 1dbc0071d7fac8d670fbd76be2ee5ca0a4af4da3 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 2 Feb 2024 13:40:20 +0100 Subject: [PATCH] draft impl for ttid --- flutter/example/lib/auto_close_screen.dart | 1 + flutter/example/lib/main.dart | 7 +- .../navigation/sentry_navigator_observer.dart | 74 ++++++++++++++++--- flutter/lib/src/sentry_flutter.dart | 18 +++-- flutter/lib/src/sentry_widget.dart | 45 ++++++++++- 5 files changed, 125 insertions(+), 20 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 15e8fac1fb..3a1866e564 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// This screen is only used to demonstrate how route navigation works. /// Init will create a child span and pop the screen after 3 seconds. diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index d43794bb90..2e4b2e7518 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -723,9 +723,10 @@ void navigateToAutoCloseScreen(BuildContext context) { Navigator.push( context, MaterialPageRoute( - settings: const RouteSettings(name: 'AutoCloseScreen'), - builder: (context) => const SentryDisplayWidget(child: AutoCloseScreen(), - )), + settings: const RouteSettings(name: 'AutoCloseScreen'), + builder: (context) => const SentryDisplayWidget( + child: AutoCloseScreen(), + )), ); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 6893d47aca..37c5a75221 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,3 +1,4 @@ +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -86,6 +87,9 @@ class SentryNavigatorObserver extends RouteObserver> { final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; + static ISentrySpan? _transaction2; + + static ISentrySpan? get transaction2 => _transaction2; ISentrySpan? _transaction; @@ -93,6 +97,8 @@ class SentryNavigatorObserver extends RouteObserver> { @internal static String? get currentRouteName => _currentRouteName; + static var startTime = DateTime.now(); + static ISentrySpan? ttidSpan; @override void didPush(Route route, Route? previousRoute) { @@ -108,7 +114,36 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); + + var routeName = route.settings.name ?? 'Unknown'; + _startTransaction(route); + + // Start timing + DateTime? approximationEndTimestamp; + int? approximationDurationMillis; + + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + approximationEndTimestamp = DateTime.now(); + approximationDurationMillis = + approximationEndTimestamp!.millisecond - startTime.millisecond; + }); + + SentryDisplayTracker().startTimeout(routeName, () { + _transaction2?.setMeasurement( + 'time_to_initial_display', approximationDurationMillis!, + unit: DurationSentryMeasurementUnit.milliSecond); + ttidSpan?.setTag('measurement', 'approximation'); + ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); + }); + } + + void freezeUIForSeconds(int seconds) { + var sw = Stopwatch()..start(); + while (sw.elapsed.inSeconds < seconds) { + // This loop will block the UI thread. + } + sw.stop(); } @override @@ -193,16 +228,26 @@ class SentryNavigatorObserver extends RouteObserver> { if (name == '/') { name = 'root ("/")'; } - final transactionContext = SentryTransactionContext( + // final transactionContext = SentryTransactionContext( + // name, + // 'navigation', + // transactionNameSource: SentryTransactionNameSource.component, + // // ignore: invalid_use_of_internal_member + // origin: SentryTraceOrigins.autoNavigationRouteObserver, + // ); + + final transactionContext2 = SentryTransactionContext( name, - 'navigation', + 'ui.load', transactionNameSource: SentryTransactionNameSource.component, // ignore: invalid_use_of_internal_member origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - _transaction = _hub.startTransactionWithContext( - transactionContext, + // IMPORTANT -> we need to wait for ttid/ttfd children to finish AND wait [autoFinishAfter] afterwards so the user can add additional spans + // right now it auto finishes when ttid/ttfd finishes but that doesn't allow the user to add spans within the idle timeout + _transaction2 = _hub.startTransactionWithContext( + transactionContext2, waitForChildren: true, autoFinishAfter: _autoFinishAfter, trimEnd: true, @@ -225,24 +270,33 @@ class SentryNavigatorObserver extends RouteObserver> { // if _enableAutoTransactions is enabled but there's no traces sample rate if (_transaction is NoOpSentrySpan) { - _transaction = null; + _transaction2 = null; return; } + startTime = DateTime.now(); + ttidSpan = _transaction2?.startChild('ui.load.initial_display'); + ttidSpan?.origin = 'auto.ui.time_to_display'; + ttidSpan?.setData('test', 'cachea'); + + // Needs to finish after 30 seconds + // If not then it will finish with status deadline exceeded + // final ttfdSpan = _transaction2?.startChild('ui.load.full_display'); + if (arguments != null) { - _transaction?.setData('route_settings_arguments', arguments); + _transaction2?.setData('route_settings_arguments', arguments); } await _hub.configureScope((scope) { - scope.span ??= _transaction; + scope.span ??= _transaction2; }); await _native?.beginNativeFramesCollection(); } - Future _finishTransaction() async { - _transaction?.status ??= SpanStatus.ok(); - await _transaction?.finish(); + Future _finishTransaction({DateTime? endTimestamp}) async { + _transaction2?.status ??= SpanStatus.ok(); + await _transaction2?.finish(endTimestamp: endTimestamp); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index d655c6b57b..3ef591183d 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -229,13 +229,21 @@ mixin SentryFlutter { options.sdk = sdk; } - static void reportInitialDisplay() { - print('reported accurate TTID!'); + static void reportInitialDisplay(BuildContext context) { + final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown'; + final endTime = DateTime.now(); + if (!SentryDisplayTracker().reportManual(routeName)) { + SentryNavigatorObserver.transaction2?.setMeasurement( + 'time_to_initial_display', + endTime.millisecond - SentryNavigatorObserver.startTime.millisecond, + unit: DurationSentryMeasurementUnit.milliSecond); + + SentryNavigatorObserver.ttidSpan?.setTag('measurement', 'manual'); + SentryNavigatorObserver.ttidSpan?.finish(endTimestamp: endTime); + } } - static void reportFullDisplay() { - - } + static void reportFullDisplay() {} @internal static SentryNative? get native => _native; diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index c9dfb6a1c0..7db077a3e5 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import '../sentry_flutter.dart'; /// This widget serves as a wrapper to include Sentry widgets such @@ -34,9 +37,10 @@ class SentryDisplayWidget extends StatefulWidget { class _SentryDisplayWidgetState extends State { @override void initState() { - // TODO: implement initState super.initState(); - SentryFlutter.reportInitialDisplay(); + WidgetsBinding.instance.addPostFrameCallback((_) { + SentryFlutter.reportInitialDisplay(context); + }); } @override @@ -44,3 +48,40 @@ class _SentryDisplayWidgetState extends State { return widget.child; } } + +class SentryDisplayTracker { + static final SentryDisplayTracker _instance = + SentryDisplayTracker._internal(); + + factory SentryDisplayTracker() { + return _instance; + } + + SentryDisplayTracker._internal(); + + final Map _manualReportReceived = {}; + final Map _timers = {}; + + void startTimeout(String routeName, Function onTimeout) { + _timers[routeName]?.cancel(); // Cancel any existing timer + _timers[routeName] = Timer(Duration(seconds: 2), () { + // Don't send if we already received a manual report or if we're on the root route e.g App start. + if (!(_manualReportReceived[routeName] ?? false)) { + onTimeout(); + } + }); + } + + bool reportManual(String routeName) { + var wasReportedAlready = _manualReportReceived[routeName] ?? false; + _manualReportReceived[routeName] = true; + _timers[routeName]?.cancel(); + return wasReportedAlready; + } + + void clearState(String routeName) { + _manualReportReceived.remove(routeName); + _timers[routeName]?.cancel(); + _timers.remove(routeName); + } +}