// 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 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 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'; late final ArgParser argParser = () { var argParser = ArgParser(); argParser.addOption(INPUT_CMDLINE_OPTION, abbr: 'i', help: '\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: ',\n' 'This option defines a mapping from the original source directory \n' 'when the instrumentation or log file was generated\n' 'to the target source directory 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: '\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 openInput(PerfArgs args) { var logger = Logger('openInput'); Stream> 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>() .transform(systemEncoding.decoder) .transform(LineSplitter()) .transform(InputConverter(args.tmpSrcDirPath, args.srcPathMap)); } /// Parse the command line arguments. PerfArgs parseArgs(List rawArgs) { ArgResults args; var perfArgs = PerfArgs(); try { args = argParser.parse(rawArgs); } on Exception catch (e) { print(e); printHelp(); exit(1); } var showHelp = args[HELP_CMDLINE_OPTION] || 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; } for (String pair in args[MAP_OPTION]) { 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 ,'); 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); } } if (args[VERY_VERBOSE_CMDLINE_OPTION] || rawArgs.contains('-vv')) { Logger.root.level = Level.FINE; } else if (args[VERBOSE_CMDLINE_OPTION]) { 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; }