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