blob: 85e82024f0fe933208f1e1433fa53c6f154924bb [file] [log] [blame]
// 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 'dart:html' as html;
import 'package:vm_service/vm_service.dart';
import 'core/message_bus.dart';
import 'debugger/debugger.dart';
import 'framework/framework.dart';
import 'globals.dart';
import 'inspector/inspector.dart';
import 'logging/logging.dart';
import 'memory/memory.dart';
import 'model/model.dart';
import 'performance/performance_screen.dart';
import 'service_registrations.dart' as registrations;
import 'settings/settings_screen.dart';
import 'timeline/timeline_screen.dart';
import 'ui/analytics.dart' as ga;
import 'ui/analytics_platform.dart' as ga_platform;
import 'ui/custom.dart';
import 'ui/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 PerfToolFramework extends Framework {
PerfToolFramework() {
html.window.onError.listen(_gAReportExceptions);
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 write them to the console to aid debugging.
print(message);
}
StatusItem isolateSelectStatus;
PSelect isolateSelect;
StatusItem connectionStatus;
Status reloadStatus;
static const _reloadActionId = 'reload-action';
static const _restartActionId = 'restart-action';
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 (Screen 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 = StatusItem();
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 = App.register(this);
screensReady.future.then(app.devToolsReady);
}
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 {
final _isFlutterApp = await serviceManager.connectedApp.isFlutterApp;
final _isFlutterWebApp = await serviceManager.connectedApp.isFlutterWebApp;
final _isProfileBuild = await serviceManager.connectedApp.isProfileBuild;
final _isAnyFlutterApp = await serviceManager.connectedApp.isAnyFlutterApp;
final _isDartWebApp = await serviceManager.connectedApp.isDartWebApp;
const notRunningFlutterApp =
'This screen is disabled because you are not running a Flutter '
'application';
const runningFlutterWeb =
'This screen is disabled because it is not yet ready for Flutter Web';
const runningProfileBuild =
'This screen is disabled because you are running a profile build of '
'your application';
const duplicateDebuggerFunctionality =
'This screen is disabled because it provides functionality already '
'available in your code editor';
const runningDartWeb =
'This screen is disabled because you are running a Dart web app';
String getDebuggerDisabledTooltip() {
if (_isFlutterWebApp) return runningFlutterWeb;
if (_isProfileBuild) return runningProfileBuild;
return duplicateDebuggerFunctionality;
}
// Collect all platform information flutter, web, chrome, versions, etc. for
// possible GA collection.
ga_platform.setupDimensions();
addScreen(InspectorScreen(
disabled: !_isAnyFlutterApp || _isProfileBuild,
disabledTooltip:
!_isAnyFlutterApp ? notRunningFlutterApp : runningProfileBuild,
));
addScreen(TimelineScreen(
disabled: !_isFlutterApp,
disabledTooltip:
_isFlutterWebApp ? runningFlutterWeb : notRunningFlutterApp,
));
addScreen(MemoryScreen(
disabled: _isFlutterWebApp || _isDartWebApp,
disabledTooltip: _isFlutterWebApp ? runningFlutterWeb : runningDartWeb,
));
addScreen(PerformanceScreen(
disabled: _isFlutterWebApp || _isDartWebApp,
disabledTooltip: _isFlutterWebApp ? runningFlutterWeb : runningDartWeb,
));
addScreen(DebuggerScreen(
disabled: _isFlutterWebApp ||
_isProfileBuild ||
isTabDisabledByQuery('debugger'),
disabledTooltip: getDebuggerDisabledTooltip(),
));
addScreen(LoggingScreen());
addScreen(SettingsScreen());
}
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 ActionButton reloadAction = ActionButton(
_reloadActionId,
FlutterIcons.hotReloadWhite,
'Hot Reload',
);
reloadAction.click(() async {
// Hide any previous status related to / restart.
reloadStatus?.dispose();
final Status status = Status(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 ActionButton restartAction = ActionButton(
_restartActionId,
FlutterIcons.hotRestartWhite,
'Hot Restart',
);
restartAction.click(() async {
// Hide any previous status related to reload / restart.
reloadStatus?.dispose();
final Status status = Status(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 = new StatusItem();
auxiliaryStatus.add(connectionStatus);
}
connectionStatus.element.text = 'no device connected';
}
}
}
class NotFoundScreen extends Screen {
NotFoundScreen() : super(name: 'Not Found', id: 'notfound');
@override
CoreElement createContent(Framework framework) {
return p(text: 'Page not found: ${html.window.location.pathname}');
}
}
class Status {
Status(this.statusLine, String initialMessage) {
item = StatusItem();
item.element.text = initialMessage;
statusLine.add(item);
}
final StatusLine statusLine;
StatusItem 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';
}
}