Fix some UI state issues with the Perfetto iFrame (#4745) * Fix some UI state issues with the Perfetto iFrame * review comments
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart index b0d6d00..aabfb71 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
@@ -5,9 +5,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; -import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; + import '../../../../../primitives/auto_dispose.dart'; import '../../../../../primitives/trace_event.dart'; import '../../../../../primitives/utils.dart'; @@ -23,13 +24,25 @@ /// app running locally. const _debugUseLocalPerfetto = false; +/// Incrementer for the Perfetto iFrame view that will live for the entire +/// DevTools lifecycle. +/// +/// A new instance of [PerfettoController] will be created for each connected +/// app and for each load of offline data. Each time [PerfettoController.init] +/// is called, we create a new [html.IFrameElement] and register it to +/// [PerfettoController.viewId] via +/// [ui.platformViewRegistry.registerViewFactory]. Each new [html.IFrameElement] +/// must have a unique id in the [PlatformViewRegistry], which +/// [_viewIdIncrementer] is used to create. +var _viewIdIncrementer = 0; + class PerfettoController extends DisposableController with AutoDisposeControllerMixin { PerfettoController(this.performanceController); final PerformanceController performanceController; - static const viewId = 'embedded-perfetto'; + late final viewId = 'embedded-perfetto-${_viewIdIncrementer++}'; /// Url when running Perfetto locally following the instructions here: /// https://perfetto.dev/docs/contributing/build-instructions#ui-development @@ -83,10 +96,30 @@ late final html.IFrameElement _perfettoIFrame; - late final Completer<void> _perfettoReady; + /// Completes when the perfetto iFrame has recevied the first event on the + /// 'onLoad' stream. + late final Completer<void> _perfettoIFrameReady; + /// Completes when the Perfetto postMessage handler is ready, which is + /// signaled by receiving a [_perfettoPong] event in response to sending a + /// [_perfettoPing] event. + late final Completer<void> _perfettoHandlerReady; + + /// Completes when the DevTools theme postMessage handler is ready, which is + /// signaled by receiving a [_devtoolsThemePong] event in response to sending + /// a [_devtoolsThemePing] event. late final Completer<void> _devtoolsThemeHandlerReady; + /// Timer that will poll until [_perfettoHandlerReady] is complete or until + /// [_pollUntilReadyTimeout] has passed. + Timer? _pollForPerfettoHandlerReady; + + /// Timer that will poll until [_devtoolsThemeHandlerReady] is complete or + /// until [_pollUntilReadyTimeout] has passed. + Timer? _pollForThemeHandlerReady; + + static const _pollUntilReadyTimeout = Duration(seconds: 10); + /// Trace events that we should load, but have not yet since the trace viewer /// is not visible (i.e. [TimelineEventsController.isActiveFeature] is false). List<TraceEventWrapper>? pendingTraceEventsToLoad; @@ -101,7 +134,8 @@ bool? pendingLoadDarkMode; void init() { - _perfettoReady = Completer(); + _perfettoIFrameReady = Completer(); + _perfettoHandlerReady = Completer(); _devtoolsThemeHandlerReady = Completer(); _perfettoIFrame = html.IFrameElement() // This url is safe because we built it ourselves and it does not include @@ -114,11 +148,18 @@ ..height = '100%' ..width = '100%'; + unawaited( + _perfettoIFrame.onLoad.first.then((_) { + _perfettoIFrameReady.complete(); + }), + ); + // ignore: undefined_prefixed_name - ui.platformViewRegistry.registerViewFactory( + final registered = ui.platformViewRegistry.registerViewFactory( viewId, (int viewId) => _perfettoIFrame, ); + assert(registered, 'Failed to register view factory for $viewId.'); html.window.addEventListener('message', _handleMessage); @@ -132,7 +173,7 @@ Future<void> onBecomingActive() async { if (pendingLoadDarkMode != null) { - await _loadStyle(pendingLoadDarkMode!); + unawaited(_loadStyle(pendingLoadDarkMode!)); } if (pendingTraceEventsToLoad != null) { await loadTrace(pendingTraceEventsToLoad!); @@ -217,7 +258,12 @@ } void _postMessage(dynamic message) async { - await _perfettoIFrameReady(); + await _perfettoIFrameReady.future; + assert( + _perfettoIFrame.contentWindow != null, + 'Something went wrong. The iFrame\'s contentWindow is null after the' + ' _perfettoIFrameReady future completed.', + ); _perfettoIFrame.contentWindow!.postMessage( message, _perfettoUrl, @@ -238,8 +284,8 @@ void _handleMessage(html.Event e) { if (e is html.MessageEvent) { - if (e.data == _perfettoPong && !_perfettoReady.isCompleted) { - _perfettoReady.complete(); + if (e.data == _perfettoPong && !_perfettoHandlerReady.isCompleted) { + _perfettoHandlerReady.complete(); } if (e.data == _devtoolsThemePong && @@ -249,36 +295,39 @@ } } - Future<void> _perfettoIFrameReady() async { - if (_perfettoIFrame.contentWindow == null) { - await _perfettoIFrame.onLoad.first; - assert( - _perfettoIFrame.contentWindow != null, - 'Something went wrong. The iFrame\'s contentWindow is null after the' - ' onLoad event.', - ); - } - } - Future<void> _pingPerfettoUntilReady() async { - while (!_perfettoReady.isCompleted) { - await Future.delayed(const Duration(microseconds: 100), () async { + if (!_perfettoHandlerReady.isCompleted) { + _pollForPerfettoHandlerReady = + Timer.periodic(const Duration(milliseconds: 200), (_) async { // Once the Perfetto UI is ready, Perfetto will receive this 'PING' // message and return a 'PONG' message, handled in [_handleMessage]. _postMessage(_perfettoPing); }); + + await _perfettoHandlerReady.future.timeout( + _pollUntilReadyTimeout, + onTimeout: () => _pollForPerfettoHandlerReady?.cancel(), + ); + _pollForPerfettoHandlerReady?.cancel(); } } Future<void> _pingDevToolsThemeHandlerUntilReady() async { if (!isExternalBuild) return; - while (!_devtoolsThemeHandlerReady.isCompleted) { - await Future.delayed(const Duration(microseconds: 100), () async { + if (!_devtoolsThemeHandlerReady.isCompleted) { + _pollForThemeHandlerReady = + Timer.periodic(const Duration(milliseconds: 200), (_) async { // Once [devtools_theme_handler.js] is ready, it will receive this // 'PING-DEVTOOLS-THEME' message and return a 'PONG-DEVTOOLS-THEME' // message, handled in [_handleMessage]. _postMessageWithId(_devtoolsThemePing, perfettoIgnore: true); }); + + await _devtoolsThemeHandlerReady.future.timeout( + _pollUntilReadyTimeout, + onTimeout: () => _pollForThemeHandlerReady?.cancel(), + ); + _pollForThemeHandlerReady?.cancel(); } } @@ -289,6 +338,8 @@ @override void dispose() { html.window.removeEventListener('message', _handleMessage); + _pollForPerfettoHandlerReady?.cancel(); + _pollForThemeHandlerReady?.cancel(); super.dispose(); } }
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart index d1e9042..9a3b0a1 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
@@ -18,8 +18,8 @@ Widget build(BuildContext context) { return Container( color: Colors.white, - child: const HtmlElementView( - viewType: PerfettoController.viewId, + child: HtmlElementView( + viewType: perfettoController.viewId, ), ); }