| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:html_shim/html.dart' as html; |
| import 'package:vm_service/vm_service.dart'; |
| |
| import 'config_specific/logger.dart'; |
| import 'core/message_bus.dart'; |
| import 'debugger/html_debugger_screen.dart'; |
| import 'framework/html_framework.dart'; |
| import 'globals.dart'; |
| import 'info/html_info_screen.dart'; |
| import 'inspector/html_inspector_screen.dart'; |
| import 'logging/html_logging_screen.dart'; |
| import 'memory/html_memory_screen.dart'; |
| import 'model/html_model.dart'; |
| import 'performance/html_performance_screen.dart'; |
| import 'server_api_client.dart'; |
| import 'service_registrations.dart' as registrations; |
| import 'timeline/html_timeline_screen.dart'; |
| import 'ui/analytics.dart' as ga; |
| import 'ui/analytics_platform.dart' as ga_platform; |
| import 'ui/html_custom.dart'; |
| import 'ui/html_elements.dart'; |
| import 'ui/icons.dart'; |
| import 'ui/primer.dart'; |
| import 'ui/ui_utils.dart'; |
| import 'utils.dart'; |
| |
| // TODO(devoncarew): make the screens more robust through restarts |
| |
| const flutterLibraryUri = 'package:flutter/src/widgets/binding.dart'; |
| const flutterWebLibraryUri = 'package:flutter_web/src/widgets/binding.dart'; |
| |
| class HtmlPerfToolFramework extends HtmlFramework { |
| HtmlPerfToolFramework() { |
| html.window.onError.listen(_gAReportExceptions); |
| |
| initDevToolsServerConnection(); |
| initGlobalUI(); |
| initTestingModel(); |
| } |
| |
| void _gAReportExceptions(html.Event e) { |
| final html.ErrorEvent errorEvent = e as html.ErrorEvent; |
| |
| final message = '${errorEvent.message}\n' |
| '${errorEvent.filename}@${errorEvent.lineno}:${errorEvent.colno}\n' |
| '${errorEvent.error}'; |
| |
| // Report exceptions with DevTools to GA. |
| ga.error(message, true); |
| |
| // Also log them to the console to aid debugging. |
| log(message, LogLevel.error); |
| } |
| |
| HtmlStatusItem isolateSelectStatus; |
| PSelect isolateSelect; |
| |
| HtmlStatusItem connectionStatus; |
| HtmlStatus reloadStatus; |
| |
| static const _reloadActionId = 'reload-action'; |
| static const _restartActionId = 'restart-action'; |
| |
| DevToolsServerApiClient devToolsServer; |
| |
| void initGlobalUI() async { |
| // Listen for clicks on the 'send feedback' button. |
| queryId('send-feedback-button').onClick.listen((_) { |
| ga.select(ga.devToolsMain, ga.feedback); |
| // TODO(devoncarew): Fill in useful product info here, like the Flutter |
| // SDK version and the version of DevTools in use. |
| html.window |
| .open('https://github.com/flutter/devtools/issues', '_feedback'); |
| }); |
| |
| await serviceManager.serviceAvailable.future; |
| await addScreens(); |
| screensReady.complete(); |
| |
| final mainNav = CoreElement.from(queryId('main-nav'))..clear(); |
| final iconNav = CoreElement.from(queryId('icon-nav'))..clear(); |
| |
| for (HtmlScreen screen in screens) { |
| final link = CoreElement('a') |
| ..add(<CoreElement>[ |
| span(c: 'octicon ${screen.iconClass}'), |
| span(text: ' ${screen.name}', c: 'optional-1060') |
| ]); |
| if (screen.disabled) { |
| link |
| ..onClick.listen((html.MouseEvent e) { |
| e.preventDefault(); |
| toast(link.tooltip); |
| }) |
| ..toggleClass('disabled', true) |
| ..tooltip = screen.disabledTooltip; |
| } else { |
| link |
| ..attributes['href'] = screen.ref |
| ..onClick.listen((html.MouseEvent e) { |
| e.preventDefault(); |
| navigateTo(screen.id); |
| }); |
| } |
| (screen.showTab ? mainNav : iconNav).add(link); |
| } |
| |
| isolateSelectStatus = HtmlStatusItem(); |
| globalStatus.add(isolateSelectStatus); |
| isolateSelect = PSelect() |
| ..small() |
| ..change(_handleIsolateSelect); |
| isolateSelectStatus.element.add(isolateSelect); |
| _rebuildIsolateSelect(); |
| |
| serviceManager.isolateManager.onIsolateCreated |
| .listen(_rebuildIsolateSelect); |
| serviceManager.isolateManager.onIsolateExited.listen(_rebuildIsolateSelect); |
| serviceManager.isolateManager.onSelectedIsolateChanged |
| .listen(_rebuildIsolateSelect); |
| |
| _initHotReloadRestartServiceListeners(); |
| |
| serviceManager.onStateChange.listen((_) { |
| _rebuildConnectionStatus(); |
| |
| if (!serviceManager.hasConnection) { |
| toast('Device connection lost.'); |
| } |
| }); |
| } |
| |
| void initTestingModel() { |
| final app = HtmlApp.register(this); |
| screensReady.future.then(app.devToolsReady); |
| } |
| |
| void initDevToolsServerConnection() { |
| // When running the debug DDC Build, the server won't be running so we |
| // can't connect to its API (for now at least, the API is optional). |
| if (isDebugBuild()) { |
| return; |
| } |
| |
| try { |
| devToolsServer = DevToolsServerApiClient(); |
| |
| // TODO(dantup): As a workaround for not being able to reconnect DevTools to |
| // a new VM yet (https://github.com/flutter/devtools/issues/989) we reload |
| // the page and pass a querystring variable to know that we need to notify |
| // the user. |
| final uri = Uri.parse(html.window.location.href); |
| if (uri.queryParameters.containsKey('notify')) { |
| final newParams = Map.of(uri.queryParameters)..remove('notify'); |
| html.window.history.pushState( |
| null, null, uri.replace(queryParameters: newParams).toString()); |
| devToolsServer.notify(); |
| } |
| |
| serviceManager.onStateChange.listen((connected) { |
| try { |
| if (connected) { |
| devToolsServer.notifyConnected(serviceManager.service.connectedUri); |
| } else { |
| devToolsServer.notifyDisconnected(); |
| } |
| } catch (e) { |
| print('Failed to notify server of connection status: $e'); |
| } |
| }); |
| } catch (e) { |
| print('Failed to connect to SSE API: $e'); |
| } |
| } |
| |
| void disableAppWithError(String title, [dynamic error]) { |
| html.document |
| .getElementById('header') |
| .children |
| .removeWhere((e) => e.id != 'title'); |
| html.document.getElementById('content').children.clear(); |
| showError(title, error); |
| } |
| |
| Future<void> addScreens() async { |
| // The types of platforms we support are: |
| // Dart CLI apps |
| // Dart web apps |
| // Flutter VM apps, in debug and profile modes |
| // Flutter web apps, using package:flutter_web |
| // Flutter web apps, using package:flutter (the unforked code) |
| |
| final app = serviceManager.connectedApp; |
| |
| final isDartWebApp = await app.isDartWebApp; |
| final isFlutterApp = await app.isFlutterApp; |
| final isDartCliApp = app.isRunningOnDartVM && !isFlutterApp; |
| final isFlutterVmApp = isFlutterApp && !isDartWebApp; |
| final isFlutterVmProfileBuild = |
| isFlutterVmApp && (await app.isProfileBuild); |
| final isFlutterWebApp = isFlutterApp && isDartWebApp; |
| |
| const notRunningFlutterMsg = |
| 'This screen is disabled because you are not running a Flutter ' |
| 'application'; |
| const runningProfileBuildMsg = |
| 'This screen is disabled because you are running a profile build of ' |
| 'your application'; |
| const notFlutterWebMsg = 'This screen does not work with Flutter web apps'; |
| const notDartWebMsg = 'This screen does not work with Dart web apps'; |
| const duplicateDebuggerFunctionalityMsg = |
| 'This screen is disabled because it provides functionality already ' |
| 'available in your code editor'; |
| |
| // Collect all platform information flutter, web, chrome, versions, etc. for |
| // possible GA collection. |
| ga_platform.setupDimensions(); |
| |
| addScreen(HtmlInspectorScreen( |
| enabled: isFlutterApp && !isFlutterVmProfileBuild, |
| disabledTooltip: isFlutterVmProfileBuild |
| ? runningProfileBuildMsg |
| : notRunningFlutterMsg, |
| )); |
| addScreen(HtmlTimelineScreen( |
| enabled: isFlutterApp && !isFlutterWebApp, |
| disabledTooltip: |
| isFlutterWebApp ? notFlutterWebMsg : notRunningFlutterMsg, |
| )); |
| addScreen(HtmlMemoryScreen( |
| enabled: isFlutterVmApp || isDartCliApp, |
| disabledTooltip: isFlutterWebApp ? notFlutterWebMsg : notDartWebMsg, |
| isProfileBuild: isFlutterVmProfileBuild, |
| )); |
| addScreen(HtmlPerformanceScreen( |
| enabled: isFlutterVmApp || isDartCliApp, |
| disabledTooltip: isFlutterWebApp ? notFlutterWebMsg : notDartWebMsg, |
| )); |
| addScreen(HtmlDebuggerScreen( |
| enabled: !isFlutterVmProfileBuild && !isTabDisabledByQuery('debugger'), |
| disabledTooltip: isFlutterVmProfileBuild |
| ? runningProfileBuildMsg |
| : duplicateDebuggerFunctionalityMsg)); |
| addScreen(HtmlLoggingScreen()); |
| addScreen(HtmlInfoScreen()); |
| } |
| |
| IsolateRef get currentIsolate => |
| serviceManager.isolateManager.selectedIsolate; |
| |
| void _handleIsolateSelect() { |
| serviceManager.isolateManager.selectIsolate(isolateSelect.value); |
| } |
| |
| void _rebuildIsolateSelect([IsolateRef _]) { |
| isolateSelect.clear(); |
| for (IsolateRef ref in serviceManager.isolateManager.isolates) { |
| isolateSelect.option(isolateName(ref), value: ref.id); |
| } |
| isolateSelect.disabled = serviceManager.isolateManager.isolates.isEmpty; |
| if (serviceManager.isolateManager.selectedIsolate != null) { |
| isolateSelect.selectedIndex = serviceManager.isolateManager.isolates |
| .indexOf(serviceManager.isolateManager.selectedIsolate); |
| } |
| } |
| |
| void _initHotReloadRestartServiceListeners() { |
| serviceManager.hasRegisteredService( |
| registrations.hotReload.service, |
| (bool reloadServiceAvailable) { |
| if (reloadServiceAvailable) { |
| _buildReloadButton(); |
| } else { |
| removeGlobalAction(_reloadActionId); |
| } |
| }, |
| ); |
| |
| serviceManager.hasRegisteredService( |
| registrations.hotRestart.service, |
| (bool reloadServiceAvailable) { |
| if (reloadServiceAvailable) { |
| _buildRestartButton(); |
| } else { |
| removeGlobalAction(_restartActionId); |
| } |
| }, |
| ); |
| } |
| |
| void _buildReloadButton() async { |
| // TODO(devoncarew): We currently create hot reload events when hot reload |
| // is initialed, and react to those events in the UI. Going forward, we'll |
| // want to instead have flutter_tools fire hot reload events, and react to |
| // them in the UI. That will mean that our UI will update appropriately |
| // even when other clients (the CLI, and IDE) initiate the hot reload. |
| |
| final HtmlActionButton reloadAction = HtmlActionButton( |
| _reloadActionId, |
| FlutterIcons.hotReloadWhite, |
| 'Hot Reload', |
| ); |
| reloadAction.click(() async { |
| // Hide any previous status related to / restart. |
| reloadStatus?.dispose(); |
| |
| final HtmlStatus status = HtmlStatus(auxiliaryStatus, 'reloading...'); |
| reloadStatus = status; |
| |
| final Stopwatch timer = Stopwatch()..start(); |
| |
| try { |
| reloadAction.disabled = true; |
| await serviceManager.performHotReload(); |
| messageBus.addEvent(BusEvent('reload.start')); |
| timer.stop(); |
| // 'reloaded in 600ms' |
| final String message = 'reloaded in ${_renderDuration(timer.elapsed)}'; |
| messageBus.addEvent(BusEvent('reload.end', data: message)); |
| status.setText(message); |
| |
| ga.select(ga.devToolsMain, ga.hotReload, timer.elapsed.inMilliseconds); |
| } catch (_) { |
| const String message = 'error performing reload'; |
| messageBus.addEvent(BusEvent('reload.end', data: message)); |
| status.setText(message); |
| } finally { |
| reloadAction.disabled = false; |
| status.timeout(); |
| } |
| }); |
| |
| addGlobalAction(reloadAction); |
| } |
| |
| void _buildRestartButton() async { |
| final HtmlActionButton restartAction = HtmlActionButton( |
| _restartActionId, |
| FlutterIcons.hotRestartWhite, |
| 'Hot Restart', |
| ); |
| restartAction.click(() async { |
| // Hide any previous status related to reload / restart. |
| reloadStatus?.dispose(); |
| |
| final HtmlStatus status = HtmlStatus(auxiliaryStatus, 'restarting...'); |
| reloadStatus = status; |
| |
| final Stopwatch timer = Stopwatch()..start(); |
| |
| try { |
| restartAction.disabled = true; |
| messageBus.addEvent(BusEvent('restart.start')); |
| await serviceManager.performHotRestart(); |
| timer.stop(); |
| // 'restarted in 1.6s' |
| final String message = 'restarted in ${_renderDuration(timer.elapsed)}'; |
| messageBus.addEvent(BusEvent('restart.end', data: message)); |
| status.setText(message); |
| ga.select(ga.devToolsMain, ga.hotRestart, timer.elapsed.inMilliseconds); |
| } catch (_) { |
| const String message = 'error performing restart'; |
| messageBus.addEvent(BusEvent('restart.end', data: message)); |
| status.setText(message); |
| } finally { |
| restartAction.disabled = false; |
| status.timeout(); |
| } |
| }); |
| |
| addGlobalAction(restartAction); |
| } |
| |
| void _rebuildConnectionStatus() { |
| if (serviceManager.hasConnection) { |
| if (connectionStatus != null) { |
| auxiliaryStatus.remove(connectionStatus); |
| connectionStatus = null; |
| } |
| } else { |
| if (connectionStatus == null) { |
| connectionStatus = HtmlStatusItem(); |
| auxiliaryStatus.add(connectionStatus); |
| } |
| connectionStatus.element.text = 'no device connected'; |
| } |
| } |
| } |
| |
| class HtmlNotFoundScreen extends HtmlScreen { |
| HtmlNotFoundScreen() : super(name: 'Not Found', id: 'notfound'); |
| |
| @override |
| CoreElement createContent(HtmlFramework framework) { |
| return p(text: 'Page not found: ${html.window.location.pathname}'); |
| } |
| } |
| |
| class HtmlStatus { |
| HtmlStatus(this.statusLine, String initialMessage) { |
| item = HtmlStatusItem(); |
| item.element.text = initialMessage; |
| |
| statusLine.add(item); |
| } |
| |
| final HtmlStatusLine statusLine; |
| HtmlStatusItem item; |
| |
| void setText(String newText) { |
| item.element.text = newText; |
| } |
| |
| void timeout() { |
| Timer(const Duration(seconds: 3), dispose); |
| } |
| |
| void dispose() { |
| statusLine.remove(item); |
| } |
| } |
| |
| String _renderDuration(Duration duration) { |
| if (duration.inMilliseconds < 1000) { |
| return '${nf.format(duration.inMilliseconds)}ms'; |
| } else { |
| return '${(duration.inMilliseconds / 1000).toStringAsFixed(1)}s'; |
| } |
| } |