| // Copyright (c) 2015, 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. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'driver.dart'; |
| import 'input_converter.dart'; |
| import 'operation.dart'; |
| |
| /// Launch and interact with the analysis server. |
| void main(List<String> rawArgs) { |
| var logger = Logger('Performance Measurement Client'); |
| logger.onRecord.listen((LogRecord rec) { |
| print(rec.message); |
| }); |
| var args = parseArgs(rawArgs); |
| |
| var driver = Driver(diagnosticPort: args.diagnosticPort); |
| var stream = openInput(args); |
| late StreamSubscription<Operation?> subscription; |
| subscription = stream.listen((Operation? op) { |
| var future = driver.perform(op!); |
| if (future != null) { |
| logger.log(Level.FINE, 'pausing operations for ${op.runtimeType}'); |
| subscription.pause(future.then((_) { |
| logger.log(Level.FINE, 'resuming operations'); |
| })); |
| } |
| }, onDone: () { |
| subscription.cancel(); |
| driver.stopServer(SHUTDOWN_TIMEOUT); |
| }, onError: (e, s) { |
| subscription.cancel(); |
| logger.log(Level.SEVERE, '$e\n$s'); |
| driver.stopServer(SHUTDOWN_TIMEOUT); |
| }); |
| driver.runComplete.then((Results results) { |
| results.printResults(); |
| }).whenComplete(() { |
| return subscription.cancel(); |
| }); |
| } |
| |
| const DIAGNOSTIC_PORT_OPTION = 'diagnosticPort'; |
| const HELP_CMDLINE_OPTION = 'help'; |
| const INPUT_CMDLINE_OPTION = 'input'; |
| const MAP_OPTION = 'map'; |
| |
| /// The amount of time to give the server to respond to a shutdown request |
| /// before forcibly terminating it. |
| const Duration SHUTDOWN_TIMEOUT = Duration(seconds: 25); |
| |
| const TMP_SRC_DIR_OPTION = 'tmpSrcDir'; |
| const VERBOSE_CMDLINE_OPTION = 'verbose'; |
| const VERY_VERBOSE_CMDLINE_OPTION = 'vv'; |
| |
| final ArgParser argParser = () { |
| var argParser = ArgParser(); |
| |
| argParser.addOption(INPUT_CMDLINE_OPTION, |
| abbr: 'i', |
| help: '<filePath>\n' |
| 'The input file specifying how this client should interact with the server.\n' |
| 'If the input file name is "stdin", then the instructions are read from standard input.'); |
| argParser.addMultiOption(MAP_OPTION, |
| abbr: 'm', |
| splitCommas: false, |
| help: '<oldSrcPath>,<newSrcPath>\n' |
| 'This option defines a mapping from the original source directory <oldSrcPath>\n' |
| 'when the instrumentation or log file was generated\n' |
| 'to the target source directory <newSrcPath> used during performance testing.\n' |
| 'Multiple mappings can be specified.\n' |
| 'WARNING: The contents of the target directory will be modified'); |
| argParser.addOption(TMP_SRC_DIR_OPTION, |
| abbr: 't', |
| help: '<dirPath>\n' |
| 'The temporary directory containing source used during performance measurement.\n' |
| 'WARNING: The contents of the target directory will be modified'); |
| argParser.addOption(DIAGNOSTIC_PORT_OPTION, |
| abbr: 'd', |
| help: 'localhost port on which server will provide diagnostic web pages'); |
| argParser.addFlag(VERBOSE_CMDLINE_OPTION, |
| abbr: 'v', help: 'Verbose logging', negatable: false); |
| argParser.addFlag(VERY_VERBOSE_CMDLINE_OPTION, |
| help: 'Extra verbose logging', negatable: false); |
| argParser.addFlag(HELP_CMDLINE_OPTION, |
| abbr: 'h', help: 'Print this help information', negatable: false); |
| return argParser; |
| }(); |
| |
| /// Open and return the input stream specifying how this client |
| /// should interact with the analysis server. |
| Stream<Operation?> openInput(PerfArgs args) { |
| var logger = Logger('openInput'); |
| Stream<List<int>> inputRaw; |
| if (args.inputPath == 'stdin') { |
| inputRaw = stdin; |
| } else { |
| inputRaw = File(args.inputPath).openRead(); |
| } |
| for (var entry in args.srcPathMap.entries) { |
| logger.log( |
| Level.INFO, |
| 'mapping source path\n' |
| ' from ${entry.oldSrcPrefix}\n to ${entry.newSrcPrefix}'); |
| } |
| logger.log(Level.INFO, 'tmpSrcDir: ${args.tmpSrcDirPath}'); |
| return inputRaw |
| .cast<List<int>>() |
| .transform(systemEncoding.decoder) |
| .transform(LineSplitter()) |
| .transform(InputConverter(args.tmpSrcDirPath, args.srcPathMap)); |
| } |
| |
| /// Parse the command line arguments. |
| PerfArgs parseArgs(List<String> rawArgs) { |
| ArgResults args; |
| var perfArgs = PerfArgs(); |
| try { |
| args = argParser.parse(rawArgs); |
| } on Exception catch (e) { |
| print(e); |
| printHelp(); |
| exit(1); |
| } |
| var helpArg = args[HELP_CMDLINE_OPTION] as bool; |
| var showHelp = helpArg || args.rest.isNotEmpty; |
| |
| var inputArg = args[INPUT_CMDLINE_OPTION]; |
| if (inputArg is! String || inputArg.isEmpty) { |
| print('missing $INPUT_CMDLINE_OPTION argument'); |
| showHelp = true; |
| } else { |
| perfArgs.inputPath = inputArg; |
| } |
| |
| var mapArg = args[MAP_OPTION] as List<Object?>; |
| for (var pair in mapArg) { |
| if (pair is String) { |
| var index = pair.indexOf(','); |
| if (index != -1 && !pair.contains(',', index + 1)) { |
| var oldSrcPrefix = _withTrailingSeparator(pair.substring(0, index)); |
| var newSrcPrefix = _withTrailingSeparator(pair.substring(index + 1)); |
| if (Directory(newSrcPrefix).existsSync()) { |
| perfArgs.srcPathMap.add(oldSrcPrefix, newSrcPrefix); |
| continue; |
| } |
| } |
| } |
| print('must specify $MAP_OPTION <oldSrcPath>,<newSrcPath>'); |
| showHelp = true; |
| } |
| |
| var tmpSrcDirPathArg = args[TMP_SRC_DIR_OPTION]; |
| if (tmpSrcDirPathArg is! String || tmpSrcDirPathArg.isEmpty) { |
| print('missing $TMP_SRC_DIR_OPTION argument'); |
| showHelp = true; |
| } else { |
| perfArgs.tmpSrcDirPath = _withTrailingSeparator(tmpSrcDirPathArg); |
| } |
| |
| var portText = args[DIAGNOSTIC_PORT_OPTION]; |
| if (portText is String) { |
| if (int.tryParse(portText) == null) { |
| print('invalid $DIAGNOSTIC_PORT_OPTION: $portText'); |
| showHelp = true; |
| } else { |
| perfArgs.diagnosticPort = int.tryParse(portText); |
| } |
| } |
| |
| var verboseArg = args[VERY_VERBOSE_CMDLINE_OPTION] as bool; |
| if (verboseArg || rawArgs.contains('-vv')) { |
| Logger.root.level = Level.FINE; |
| } else if (verboseArg) { |
| Logger.root.level = Level.INFO; |
| } else { |
| Logger.root.level = Level.WARNING; |
| } |
| |
| if (showHelp) { |
| printHelp(); |
| exit(1); |
| } |
| |
| return perfArgs; |
| } |
| |
| void printHelp() { |
| print(''); |
| print('Launch and interact with the AnalysisServer'); |
| print(''); |
| print(argParser.usage); |
| } |
| |
| /// Ensure that the given path has a trailing separator |
| String _withTrailingSeparator(String dirPath) { |
| if (dirPath.length > 4) { |
| if (!dirPath.endsWith(path.separator)) { |
| return '$dirPath${path.separator}'; |
| } |
| } |
| return dirPath; |
| } |
| |
| /// The performance measurement arguments specified on the command line. |
| class PerfArgs { |
| /// The file path of the instrumentation or log file |
| /// used to drive performance measurement, |
| /// or 'stdin' if this information should be read from standard input. |
| late String inputPath; |
| |
| /// A mapping from the original source directory |
| /// when the instrumentation or log file was generated |
| /// to the target source directory used during performance testing. |
| final PathMap srcPathMap = PathMap(); |
| |
| /// The temporary directory containing source used during performance |
| /// measurement. |
| late String tmpSrcDirPath; |
| |
| /// The diagnostic port for Analysis Server or `null` if none. |
| int? diagnosticPort; |
| } |