| // 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:io'; | |
| import 'dart:math' show max, sqrt; | |
| import 'package:logging/logging.dart'; | |
| import 'package:path/path.dart'; | |
| import '../../integration_test/support/integration_test_methods.dart'; | |
| import '../../integration_test/support/integration_tests.dart'; | |
| import 'operation.dart'; | |
| /// [Driver] launches and manages an instance of analysis server, | |
| /// reads a stream of operations, sends requests to analysis server | |
| /// based upon those operations, and evaluates the results. | |
| class Driver extends IntegrationTest { | |
| /// The amount of time to give the server to respond to a shutdown request | |
| /// before forcibly terminating it. | |
| static const Duration shutdownTimeout = Duration(seconds: 5); | |
| final Logger logger = Logger('Driver'); | |
| /// The diagnostic port for Analysis Server or `null` if none. | |
| final int? diagnosticPort; | |
| /// A flag indicating whether the server is running. | |
| bool running = false; | |
| @override | |
| late Server server; | |
| /// The results collected while running analysis server. | |
| final Results results = Results(); | |
| /// The [Completer] for [runComplete]. | |
| final Completer<Results> _runCompleter = Completer<Results>(); | |
| new({this.diagnosticPort}); | |
| /// Return a [Future] that completes with the [Results] of running | |
| /// the analysis server once all operations have been performed. | |
| Future<Results> get runComplete => _runCompleter.future; | |
| /// Perform the given operation. | |
| /// | |
| /// Return a [Future] that completes when the next operation can be performed, | |
| /// or `null` if the next operation can be performed immediately | |
| Future<void>? perform(Operation op) { | |
| return op.perform(this); | |
| } | |
| /// Send a command to the server. | |
| /// | |
| /// An 'id' will be automatically assigned. The returned [Future] will be | |
| /// completed when the server acknowledges the command with a response. If | |
| /// the server acknowledges the command with a normal (non-error) response, | |
| /// the future will be completed with the 'result' field from the response. | |
| /// If the server acknowledges the command with an error response, the future | |
| /// will be completed with an error. | |
| Future<Map<String, Object?>?> send( | |
| String method, | |
| Map<String, dynamic> params, | |
| ) { | |
| return server.send(method, params); | |
| } | |
| /// Launch the analysis server. | |
| /// | |
| /// Return a [Future] that completes when analysis server has started. | |
| Future<void> startServer() { | |
| logger.log(Level.FINE, 'starting server'); | |
| server = Server(); | |
| var serverConnected = Completer<void>(); | |
| onServerConnected.listen((_) { | |
| logger.log(Level.FINE, 'connected to server'); | |
| serverConnected.complete(); | |
| }); | |
| running = true; | |
| var dartSdkPath = dirname(dirname(Platform.resolvedExecutable)); | |
| return server | |
| .start(dartSdkPath: dartSdkPath, diagnosticPort: diagnosticPort) | |
| .then((params) { | |
| server.listenToOutput(dispatchNotification); | |
| server.exitCode.then((_) { | |
| logger.log(Level.FINE, 'server stopped'); | |
| running = false; | |
| _resultsReady(); | |
| }); | |
| return serverConnected.future; | |
| }); | |
| } | |
| /// Shutdown the analysis server if it is running. | |
| Future<void> stopServer([Duration timeout = shutdownTimeout]) async { | |
| if (running) { | |
| logger.log(Level.FINE, 'requesting server shutdown'); | |
| // Give the server a short time to comply with the shutdown request; if it | |
| // doesn't exit, then forcibly terminate it. | |
| unawaited(sendServerShutdown()); | |
| await server.exitCode.timeout( | |
| timeout, | |
| onTimeout: () { | |
| return server.kill('server failed to exit'); | |
| }, | |
| ); | |
| } | |
| _resultsReady(); | |
| } | |
| /// If not already complete, signal the completer with the collected results. | |
| void _resultsReady() { | |
| if (!_runCompleter.isCompleted) { | |
| _runCompleter.complete(results); | |
| } | |
| } | |
| } | |
| /// [Measurement] tracks elapsed time for a given operation. | |
| class Measurement { | |
| final String tag; | |
| final bool notification; | |
| final List<Duration> elapsedTimes = <Duration>[]; | |
| int errorCount = 0; | |
| int unexpectedResultCount = 0; | |
| new(this.tag, this.notification); | |
| int get count => elapsedTimes.length; | |
| void printSummary(int keyLen) { | |
| var count = 0; | |
| var maxTime = elapsedTimes[0]; | |
| var minTime = elapsedTimes[0]; | |
| var totalTimeMicros = 0; | |
| for (var elapsed in elapsedTimes) { | |
| ++count; | |
| var timeMicros = elapsed.inMicroseconds; | |
| maxTime = maxTime.compareTo(elapsed) > 0 ? maxTime : elapsed; | |
| minTime = minTime.compareTo(elapsed) < 0 ? minTime : elapsed; | |
| totalTimeMicros += timeMicros; | |
| } | |
| var meanTime = (totalTimeMicros / count).round(); | |
| var sorted = elapsedTimes.toList()..sort(); | |
| var time90th = sorted[(sorted.length * 0.90).round() - 1]; | |
| var time99th = sorted[(sorted.length * 0.99).round() - 1]; | |
| var differenceFromMeanSquared = 0; | |
| for (var elapsed in elapsedTimes) { | |
| var timeMicros = elapsed.inMicroseconds; | |
| var differenceFromMean = timeMicros - meanTime; | |
| differenceFromMeanSquared += differenceFromMean * differenceFromMean; | |
| } | |
| var variance = differenceFromMeanSquared / count; | |
| var standardDeviation = sqrt(variance).round(); | |
| var buffer = StringBuffer(); | |
| buffer.writePadRight(tag, keyLen); | |
| buffer.writePadLeft(count.toString(), 6); | |
| buffer.writePadLeft(errorCount.toString(), 6); | |
| buffer.writePadLeft(unexpectedResultCount.toString(), 6); | |
| buffer.writeDuration(Duration(microseconds: meanTime)); | |
| buffer.writeDuration(time90th); | |
| buffer.writeDuration(time99th); | |
| buffer.writeDuration(Duration(microseconds: standardDeviation)); | |
| buffer.writeDuration(minTime); | |
| buffer.writeDuration(maxTime); | |
| buffer.writeDuration(Duration(microseconds: totalTimeMicros)); | |
| print(buffer.toString()); | |
| } | |
| void record(bool success, Duration elapsed) { | |
| if (!success) { | |
| ++errorCount; | |
| } | |
| elapsedTimes.add(elapsed); | |
| } | |
| void recordUnexpectedResults() { | |
| ++unexpectedResultCount; | |
| } | |
| } | |
| /// [Results] contains information gathered by [Driver] while running the | |
| /// analysis server. | |
| class Results { | |
| Map<String, Measurement> measurements = <String, Measurement>{}; | |
| /// Display results on stdout. | |
| void printResults() { | |
| print(''); | |
| print('=================================================================='); | |
| print(''); | |
| var sortedEntries = measurements.entries.toList(); | |
| sortedEntries.sort((a, b) => a.key.compareTo(b.key)); | |
| var keyLen = sortedEntries | |
| .map((e) => e.key) | |
| .fold(0, (int len, String key) => max(len, key.length)); | |
| _printGroupHeader('Request/Response', keyLen); | |
| var totalCount = 0; | |
| var totalErrorCount = 0; | |
| var totalUnexpectedResultCount = 0; | |
| for (var entry in sortedEntries) { | |
| var m = entry.value; | |
| if (!m.notification) { | |
| m.printSummary(keyLen); | |
| totalCount += m.count; | |
| totalErrorCount += m.errorCount; | |
| totalUnexpectedResultCount += m.unexpectedResultCount; | |
| } | |
| } | |
| _printTotals( | |
| keyLen, | |
| totalCount, | |
| totalErrorCount, | |
| totalUnexpectedResultCount, | |
| ); | |
| print(''); | |
| _printGroupHeader('Notifications', keyLen); | |
| for (var entry in sortedEntries) { | |
| var m = entry.value; | |
| if (m.notification) { | |
| m.printSummary(keyLen); | |
| } | |
| } | |
| // TODO(danrubel): print warnings if driver caches are not empty. | |
| print(''' | |
| (1) uxr = UneXpected Results or responses received from the server | |
| that do not match the recorded response for that request. | |
| (2) all times in milliseconds'''); | |
| } | |
| /// Record the elapsed time for the given operation. | |
| void record( | |
| String tag, | |
| Duration elapsed, { | |
| bool notification = false, | |
| bool success = true, | |
| }) { | |
| var measurement = measurements[tag]; | |
| if (measurement == null) { | |
| measurement = Measurement(tag, notification); | |
| measurements[tag] = measurement; | |
| } | |
| measurement.record(success, elapsed); | |
| } | |
| void recordUnexpectedResults(String tag) { | |
| measurements[tag]!.recordUnexpectedResults(); | |
| } | |
| static void _printGroupHeader(String groupName, int keyLength) { | |
| var buffer = StringBuffer(); | |
| buffer.writePadRight(groupName, keyLength); | |
| buffer.writePadLeft('count', 6); | |
| buffer.writePadLeft('error', 6); | |
| buffer.writePadLeft('uxr(1)', 6); | |
| buffer.write(' '); | |
| buffer.writePadRight('mean(2)', 15); | |
| buffer.writePadRight('90th', 15); | |
| buffer.writePadRight('99th', 15); | |
| buffer.writePadRight('std-dev', 15); | |
| buffer.writePadRight('minimum', 15); | |
| buffer.writePadRight('maximum', 15); | |
| buffer.writePadRight('total', 15); | |
| print(buffer.toString()); | |
| } | |
| static void _printTotals( | |
| int keyLength, | |
| int totalCount, | |
| int totalErrorCount, | |
| int totalUnexpectedResultCount, | |
| ) { | |
| var buffer = StringBuffer(); | |
| buffer.writePadRight('Totals', keyLength); | |
| buffer.writePadLeft(totalCount.toString(), 6); | |
| buffer.writePadLeft(totalErrorCount.toString(), 6); | |
| buffer.writePadLeft(totalUnexpectedResultCount.toString(), 6); | |
| print(buffer.toString()); | |
| } | |
| } | |
| extension on StringBuffer { | |
| void writeDuration(Duration duration) { | |
| writePadLeft(duration.inMilliseconds.toString(), 15); | |
| } | |
| void writePadLeft(String text, int keyLength) { | |
| write(text.padLeft(keyLength)); | |
| write(' '); | |
| } | |
| void writePadRight(String text, int keyLength) { | |
| write(text.padRight(keyLength)); | |
| write(' '); | |
| } | |
| } |