| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// Example that illustrates how to use the incremental compiler and trigger a |
| /// hot-reload on the VM after recompiling the application. |
| /// |
| /// This example resembles the `run` command in flutter-tools. It creates an |
| /// interactive command-line program that waits for the user to tap a key to |
| /// trigger a recompile and reload. |
| /// |
| /// The following instructions assume a linux checkout of the SDK: |
| /// * Build the SDK |
| /// |
| /// ``` |
| /// ./tools/build.py -m release |
| /// ``` |
| /// |
| /// * On one terminal (terminal A), start this script and point it to an |
| /// example program "foo.dart" and keep the job running. A good example |
| /// program would do something periodically, so you can see the effect |
| /// of a hot-reload while the app is running. |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart pkg/front_end/example/incremental_reload/run.dart foo.dart out.dill |
| /// ``` |
| /// |
| /// * Trigger an initial compile of the program by hitting the "c" key in |
| /// terminal A. |
| /// |
| /// * On another terminal (terminal B), start the program on the VM, with the |
| /// service-protocol enabled and provide the precompiled platform libraries: |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart --enable-vm-service --platform=out/ReleaseX64/patched_sdk/platform.dill out.dill |
| /// ``` |
| /// |
| /// * Modify the orginal program |
| /// |
| /// * In terminal A, hit the "r" key to trigger a recompile and hot reload. |
| /// |
| /// * See the changed program in terminal B |
| library front_end.example.incremental_reload.run; |
| |
| import 'dart:io'; |
| import 'dart:async'; |
| import 'dart:convert' show ASCII; |
| |
| import 'package:args/args.dart'; |
| import 'package:kernel/target/targets.dart'; |
| |
| import '../../test/tool/reload.dart'; |
| |
| import 'compiler_with_invalidation.dart'; |
| |
| ArgParser argParser = new ArgParser(allowTrailingOptions: true) |
| ..addOption('sdk-root', help: 'Path to sdk for compilation') |
| ..addOption('output', |
| help: 'Output dill file', defaultsTo: 'out.dill', abbr: 'o') |
| ..addOption('target', |
| help: 'One of none, vm, vm_fasta, vmcc, vmreify, flutter, flutter_fasta', |
| defaultsTo: 'vm'); |
| |
| String usage = ''' |
| Usage: dart [options] input.dart |
| |
| Runs console-driven incremental compiler. |
| |
| Options: |
| ${argParser.usage} |
| '''; |
| |
| RemoteVm remoteVm = new RemoteVm(); |
| AnsiTerminal terminal = new AnsiTerminal(); |
| |
| main(List<String> args) async { |
| ArgResults options = argParser.parse(args); |
| if (options.rest.isEmpty) { |
| print('Need an input file'); |
| print(usage); |
| exit(1); |
| } |
| |
| var compiler = await createIncrementalCompiler(options.rest[0], |
| sdkRoot: |
| options['sdk-root'] != null ? Uri.parse(options['sdk-root']) : null, |
| target: targets[options['target']](new TargetFlags())); |
| var outputUri = Uri.base.resolve(options['output']); |
| |
| showHeader(); |
| listenOnKeyPress(compiler, outputUri) |
| .whenComplete(() => remoteVm.disconnect()); |
| } |
| |
| /// Implements the interactive UI by listening for input keys from the user. |
| Future listenOnKeyPress(IncrementalCompiler compiler, Uri outputUri) { |
| var completer = new Completer(); |
| terminal.singleCharMode = true; |
| StreamSubscription subscription; |
| subscription = terminal.onCharInput.listen((String char) async { |
| try { |
| CompilationResult compilationResult; |
| ReloadResult reloadResult; |
| switch (char.trim()) { |
| case 'r': |
| compilationResult = await rebuild(compiler, outputUri); |
| if (!compilationResult.errorSeen && |
| compilationResult.program != null && |
| compilationResult.program.libraries.isNotEmpty) { |
| reloadResult = await reload(outputUri); |
| } |
| break; |
| case 'c': |
| compilationResult = await rebuild(compiler, outputUri); |
| break; |
| case 'l': |
| reloadResult = await reload(outputUri); |
| break; |
| case 'q': |
| terminal.singleCharMode = false; |
| print(''); |
| subscription.cancel(); |
| completer.complete(null); |
| break; |
| default: |
| break; |
| } |
| if (compilationResult != null || reloadResult != null) { |
| reportStats(compilationResult, reloadResult, outputUri); |
| } |
| } catch (e) { |
| terminal.singleCharMode = false; |
| subscription.cancel(); |
| completer.completeError(null); |
| rethrow; |
| } |
| }, onError: (e) { |
| terminal.singleCharMode = false; |
| subscription.cancel(); |
| completer.completeError(null); |
| }); |
| |
| return completer.future; |
| } |
| |
| /// Request a reload and gather timing metrics. |
| Future<ReloadResult> reload(outputUri) async { |
| var result = new ReloadResult(); |
| var reloadTimer = new Stopwatch()..start(); |
| var reloadResult = await remoteVm.reload(outputUri); |
| reloadTimer.stop(); |
| result.reloadTime = reloadTimer.elapsedMilliseconds; |
| result.errorSeen = false; |
| result.errorDetails; |
| if (!reloadResult['success']) { |
| result.errorSeen = true; |
| result.errorDetails = reloadResult['details']['notices'].first['message']; |
| } |
| return result; |
| } |
| |
| /// Results from requesting a hot reload. |
| class ReloadResult { |
| /// How long it took to do the hot-reload in the VM. |
| int reloadTime = 0; |
| |
| /// Whether we saw errors during compilation or reload. |
| bool errorSeen = false; |
| |
| /// Error message when [errorSeen] is true. |
| String errorDetails; |
| } |
| |
| /// This script shows stats about each reload on the terminal in a table form. |
| /// This function prints out the header of such table. |
| showHeader() { |
| print(terminal.bolden('Press a key to trigger a command:')); |
| print(terminal.bolden(' r: incremental compile + reload')); |
| print(terminal.bolden(' c: incremental compile w/o reload')); |
| print(terminal.bolden(' l: reload w/o recompile')); |
| print(terminal.bolden(' q: quit')); |
| print(terminal.bolden( |
| '# Files Files % ------- Time ------------------------- Binary\n' |
| ' Modified Sent Total Check Compile Reload Total Avg Size ')); |
| } |
| |
| /// Whether to show stats as a single line (override metrics on each request) |
| const bool singleLine = false; |
| |
| var total = 0; |
| var iter = 0; |
| var timeSum = 0; |
| var lastLine = 0; |
| |
| /// Show stats about a recompile and reload. |
| reportStats(CompilationResult compilationResult, ReloadResult reloadResult, |
| Uri outputUri) { |
| compilationResult ??= new CompilationResult(); |
| reloadResult ??= new ReloadResult(); |
| int changed = compilationResult.changed; |
| int updated = compilationResult.program?.libraries?.length ?? 0; |
| int totalFiles = compilationResult.totalFiles; |
| int invalidateTime = compilationResult.invalidateTime; |
| int compileTime = compilationResult.compileTime; |
| int reloadTime = reloadResult.reloadTime; |
| bool errorSeen = compilationResult.errorSeen || reloadResult.errorSeen; |
| String errorDetails = |
| compilationResult.errorDetails ?? reloadResult.errorDetails; |
| |
| var totalTime = invalidateTime + compileTime + reloadTime; |
| timeSum += totalTime; |
| total++; |
| iter++; |
| var avgTime = (timeSum / total).truncate(); |
| var size = new File.fromUri(outputUri).statSync().size; |
| |
| var percent = (100 * updated / totalFiles).toStringAsFixed(0); |
| var line = '${_padl(iter, 3)}: ' |
| '${_padl(changed, 8)} ${_padl(updated, 5)} ${_padl(percent, 4)}% ' |
| '${_padl(invalidateTime, 5)} ms ' |
| '${_padl(compileTime, 5)} ms ' |
| '${_padl(reloadTime, 5)} ms ' |
| '${_padl(totalTime, 5)} ms ' |
| '${_padl(avgTime, 5)} ms ' |
| '${_padl(size, 5)}b'; |
| var len = line.length; |
| if (singleLine) stdout.write('\r'); |
| stdout.write((errorSeen) ? terminal.red(line) : terminal.green(line)); |
| if (!singleLine) stdout.write('\n'); |
| if (errorSeen) { |
| if (!singleLine) errorDetails = ' error: $errorDetails\n'; |
| len += errorDetails.length; |
| stdout.write(errorDetails); |
| } |
| if (singleLine) { |
| var diff = " " * (lastLine - len); |
| stdout.write(diff); |
| } |
| lastLine = len; |
| } |
| |
| _padl(x, n) { |
| var s = '$x'; |
| return ' ' * (n - s.length) + s; |
| } |
| |
| /// Helper to control an ANSI terminal (adapted from flutter_tools) |
| class AnsiTerminal { |
| static const String _bold = '\u001B[1m'; |
| static const String _green = '\u001B[32m'; |
| static const String _red = '\u001B[31m'; |
| static const String _reset = '\u001B[0m'; |
| static const String _clear = '\u001B[2J\u001B[H'; |
| |
| static const int _ENXIO = 6; |
| static const int _ENOTTY = 25; |
| static const int _ENETRESET = 102; |
| static const int _INVALID_HANDLE = 6; |
| |
| /// Setting the line mode can throw for some terminals (with "Operation not |
| /// supported on socket"), but the error can be safely ignored. |
| static const List<int> _lineModeIgnorableErrors = const <int>[ |
| _ENXIO, |
| _ENOTTY, |
| _ENETRESET, |
| _INVALID_HANDLE, |
| ]; |
| |
| String bolden(String message) => wrap(message, _bold); |
| |
| String green(String message) => wrap(message, _green); |
| |
| String red(String message) => wrap(message, _red); |
| |
| String wrap(String message, String escape) { |
| final StringBuffer buffer = new StringBuffer(); |
| for (String line in message.split('\n')) |
| buffer.writeln('$escape$line$_reset'); |
| final String result = buffer.toString(); |
| // avoid introducing a new newline to the emboldened text |
| return (!message.endsWith('\n') && result.endsWith('\n')) |
| ? result.substring(0, result.length - 1) |
| : result; |
| } |
| |
| String clearScreen() => _clear; |
| |
| set singleCharMode(bool value) { |
| // TODO(goderbauer): instead of trying to set lineMode and then catching |
| // [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is |
| // connected to a terminal or not. |
| // (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.) |
| try { |
| // The order of setting lineMode and echoMode is important on Windows. |
| if (value) { |
| stdin.echoMode = false; |
| stdin.lineMode = false; |
| } else { |
| stdin.lineMode = true; |
| stdin.echoMode = true; |
| } |
| } on StdinException catch (error) { |
| if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode)) rethrow; |
| } |
| } |
| |
| /// Return keystrokes from the console. |
| /// |
| /// Useful when the console is in [singleCharMode]. |
| Stream<String> get onCharInput => stdin.transform(ASCII.decoder); |
| } |