| // Copyright (c) 2025, 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 'dart:typed_data'; | |
| import 'io_utils.dart'; | |
| import 'legacy_messages.dart'; | |
| import 'lsp_messages.dart'; | |
| import 'utils.dart'; | |
| abstract class DartLanguageServerBenchmark { | |
| int verbosity = 0; | |
| final Uri repoDir = computeRepoDirUri(); | |
| late final Process p; | |
| late final Timer longRunningRequestsTimer; | |
| bool _launched = false; | |
| bool _exited = false; | |
| Completer<bool> _analyzingCompleter = Completer(); | |
| final _buffer = _Uint8ListHelper(); | |
| int? _headerContentLength; | |
| bool _printedVmServiceStuff = false; | |
| final String executableToUse; | |
| bool latestIsAnalyzing = false; | |
| /// There's something weird about getting (several) id 3's that wasn't | |
| /// requested... | |
| int largestIdSeen = 3; | |
| final RegExp _newLineRegExp = RegExp('\r?\n'); | |
| final Map<int, OutstandingRequest> _outstandingRequestsWithId = {}; | |
| bool _addedInitialAnalysisDuration = false; | |
| List<DurationInfo> durationInfo = []; | |
| List<MemoryInfo> memoryInfo = []; | |
| final bool _lsp; | |
| new(List<String> args, {required bool useLspProtocol}) | |
| : _lsp = useLspProtocol, | |
| executableToUse = extractDartParamOrDefault(args) { | |
| _checkCorrectDart(); | |
| } | |
| List<Uri> get additionalWorkspaceUris => const []; | |
| Uri? get cacheFolder => null; | |
| LaunchFrom get launchFrom => LaunchFrom.source; | |
| Uri get rootUri; | |
| Future<void> afterInitialization(); | |
| void exit() { | |
| _exited = true; | |
| longRunningRequestsTimer.cancel(); | |
| if (Platform.isLinux) { | |
| try { | |
| File f = File('/proc/${p.pid}/status'); | |
| for (var line in f.readAsLinesSync()) { | |
| if (line.startsWith('VmPeak:')) { | |
| memoryInfo.addInfoMaybe( | |
| 'peak virtual memory size', | |
| _extractSizeFromProcStatusMemoryLine(line), | |
| ); | |
| } else if (line.startsWith('VmSize:')) { | |
| memoryInfo.addInfoMaybe( | |
| 'total program size (virtual)', | |
| _extractSizeFromProcStatusMemoryLine(line), | |
| ); | |
| } else if (line.startsWith('VmHWM:')) { | |
| memoryInfo.addInfoMaybe( | |
| 'peak resident set size ("high water mark")', | |
| _extractSizeFromProcStatusMemoryLine(line), | |
| ); | |
| } else if (line.startsWith('VmRSS:')) { | |
| memoryInfo.addInfoMaybe( | |
| 'size of memory portions (rss)', | |
| _extractSizeFromProcStatusMemoryLine(line), | |
| ); | |
| } | |
| } | |
| } catch (e) { | |
| // ignore | |
| } | |
| } | |
| p.kill(); | |
| } | |
| Future<void> run() async { | |
| if (_lsp) { | |
| await _runLsp(); | |
| } else { | |
| await _runLegacy(); | |
| } | |
| } | |
| Future<OutstandingRequest?> send(Map<String, dynamic> json) async { | |
| var result = sendNoFlush(json); | |
| await p.stdin.flush(); | |
| return result; | |
| } | |
| OutstandingRequest? sendNoFlush(Map<String, dynamic> json) { | |
| // Mostly copied from | |
| // pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart | |
| var jsonEncodedBody = jsonEncode(json); | |
| var utf8EncodedBody = utf8.encode(jsonEncodedBody); | |
| var header = | |
| 'Content-Length: ${utf8EncodedBody.length}\r\n' | |
| 'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n'; | |
| var asciiEncodedHeader = ascii.encode(header); | |
| OutstandingRequest? result; | |
| dynamic possibleId = json['id']; | |
| if (!_lsp && possibleId is String && int.tryParse(possibleId) != null) { | |
| possibleId = int.tryParse(possibleId); | |
| } | |
| if (possibleId is int) { | |
| if (possibleId > largestIdSeen) { | |
| largestIdSeen = possibleId; | |
| } | |
| result = OutstandingRequest(); | |
| _outstandingRequestsWithId[possibleId] = result; | |
| if (verbosity > 2) { | |
| print('Sending message with id $possibleId'); | |
| } | |
| } | |
| if (verbosity > 3) { | |
| print('Sending message: $jsonEncodedBody'); | |
| } | |
| if (_lsp) { | |
| // Header is always ascii, body is always utf8! | |
| p.stdin.add(asciiEncodedHeader); | |
| } | |
| p.stdin.add(utf8EncodedBody); | |
| if (!_lsp) { | |
| p.stdin.add(const [10]); | |
| } | |
| if (verbosity > 2) { | |
| print('\n\nMessage sent\n\n'); | |
| } | |
| return result; | |
| } | |
| Future<bool> waitUntilIsAnalyzingChanges() async { | |
| return await _analyzingCompleter.future; | |
| } | |
| Future<void> waitWhileAnalyzing() async { | |
| // Wait until it's done analyzing. | |
| bool isAnalyzing = await _analyzingCompleter.future; | |
| Stopwatch stopwatch = Stopwatch()..start(); | |
| while (isAnalyzing) { | |
| isAnalyzing = await _analyzingCompleter.future; | |
| } | |
| if (verbosity >= 0) { | |
| print('isAnalyzing is now done after ${stopwatch.elapsed}'); | |
| } | |
| if (!_addedInitialAnalysisDuration) { | |
| _addedInitialAnalysisDuration = true; | |
| durationInfo.add(DurationInfo('Initial analysis', stopwatch.elapsed)); | |
| } | |
| } | |
| void _checkCorrectDart() { | |
| Uri exe = Uri.base.resolveUri(Uri.file(executableToUse)); | |
| Uri librariesDart = exe.resolve( | |
| '../lib/_internal/sdk_library_metadata/lib/libraries.dart', | |
| ); | |
| if (!File.fromUri(librariesDart).existsSync()) { | |
| throw 'Execute with a dart that has ' | |
| "'../lib/_internal/sdk_library_metadata/lib/libraries.dart' " | |
| 'available (e.g. out/ReleaseX64/dart-sdk/bin/dart)'; | |
| } | |
| } | |
| void _checkLongRunningRequests(Timer timer) { | |
| if (verbosity < 0) return; | |
| bool reportedSomething = false; | |
| for (MapEntry<int, OutstandingRequest> waitingFor | |
| in _outstandingRequestsWithId.entries) { | |
| if (waitingFor.value.stopwatch.elapsed > const Duration(seconds: 1)) { | |
| if (!reportedSomething) { | |
| print('----'); | |
| reportedSomething = true; | |
| } | |
| print( | |
| '==> Has been waiting for required #${waitingFor.key} for ' | |
| '${waitingFor.value.stopwatch.elapsed}', | |
| ); | |
| } | |
| } | |
| if (reportedSomething) { | |
| print('----'); | |
| } else { | |
| // print(" -- not waiting for anything -- "); | |
| } | |
| } | |
| /// Copied from pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart. | |
| bool _endsWithCrLfCrLf() { | |
| var l = _buffer.length; | |
| return l > 4 && | |
| _buffer[l - 1] == 10 && | |
| _buffer[l - 2] == 13 && | |
| _buffer[l - 3] == 10 && | |
| _buffer[l - 4] == 13; | |
| } | |
| bool _endsWithLf() { | |
| var l = _buffer.length; | |
| return l > 1 && _buffer[l - 1] == 10; | |
| } | |
| int? _extractSizeFromProcStatusMemoryLine(String line) { | |
| int colon = line.indexOf(':'); | |
| if (colon < 0) return null; | |
| int kb = line.indexOf('kB', colon); | |
| if (kb < 0) return null; | |
| String substring = line.substring(colon + 1, kb).trim(); | |
| return int.tryParse(substring); | |
| } | |
| Future<void> _initializeLegacy() async { | |
| await send(LegacyMessages.getVersion(largestIdSeen + 1)); | |
| await send(LegacyMessages.setSubscriptionsStatus(largestIdSeen + 1)); | |
| await send(LegacyMessages.updateOptions(largestIdSeen + 1)); | |
| await send(LegacyMessages.setClientCapabilities(largestIdSeen + 1)); | |
| await send( | |
| LegacyMessages.setAnalysisRoots(largestIdSeen + 1, [ | |
| rootUri, | |
| ...additionalWorkspaceUris, | |
| ]), | |
| ); | |
| // Wait until it's done analyzing. | |
| await waitWhileAnalyzing(); | |
| } | |
| Future<void> _initializeLsp() async { | |
| OutstandingRequest? request = await send( | |
| LspMessages.initMessage(pid, rootUri, additionalWorkspaceUris), | |
| ); | |
| await request?.completer.future; | |
| await send(LspMessages.initNotification); | |
| // Wait until it's done analyzing. | |
| await waitWhileAnalyzing(); | |
| } | |
| Future<void> _launch({required bool lsp}) async { | |
| if (_launched) throw 'Already launched'; | |
| _launched = true; | |
| List<String> cacheFolderArgs = []; | |
| if (cacheFolder != null) { | |
| cacheFolderArgs.add('--cache'); | |
| cacheFolderArgs.add(cacheFolder!.toFilePath()); | |
| } | |
| switch (launchFrom) { | |
| case LaunchFrom.source: | |
| File serverFile = File.fromUri( | |
| repoDir.resolve('pkg/analysis_server/bin/server.dart'), | |
| ); | |
| if (!serverFile.existsSync()) { | |
| throw "Couldn't find 'analysis_server/bin/server.dart' " | |
| 'expected it at $serverFile'; | |
| } | |
| // TODO(jensj): Option of passing --profiler | |
| p = await Process.start(executableToUse, [ | |
| '--enable-vm-service', | |
| '--profiler', | |
| serverFile.path, | |
| lsp ? '--protocol=lsp' : '--protocol=analyzer', | |
| '--port=9102', | |
| ...cacheFolderArgs, | |
| ]); | |
| case LaunchFrom.dart: | |
| // TODO(jensj): Option of wrapping in `perf record -g` call. | |
| p = await Process.start(executableToUse, [ | |
| 'language-server', | |
| lsp ? '--protocol=lsp' : '--protocol=analyzer', | |
| '--port=9102', | |
| ...cacheFolderArgs, | |
| ]); | |
| case LaunchFrom.aotWithPerf: | |
| case LaunchFrom.aot: | |
| File serverFile = File.fromUri( | |
| repoDir.resolve('pkg/analysis_server/bin/server.aot'), | |
| ); | |
| if (!serverFile.existsSync()) { | |
| throw "Couldn't find 'analysis_server/bin/server.aot' " | |
| 'expected it at $serverFile'; | |
| } | |
| // TODO(jensj): Option of passing --profiler | |
| Uri dart = Uri.base.resolveUri(Uri.file(executableToUse)); | |
| File aotRuntime = File.fromUri(dart.resolve('dartaotruntime')); | |
| if (!aotRuntime.existsSync()) { | |
| throw "Couldn't find 'dartaotruntime' expected it at $aotRuntime"; | |
| } | |
| p = await Process.start(aotRuntime.path, [ | |
| serverFile.path, | |
| lsp ? '--protocol=lsp' : '--protocol=analyzer', | |
| '--port=9102', | |
| ...cacheFolderArgs, | |
| ]); | |
| if (launchFrom == LaunchFrom.aotWithPerf) { | |
| await Process.start('perf', [ | |
| 'record', | |
| '-p', | |
| '${p.pid}', | |
| '-g', | |
| ], mode: ProcessStartMode.inheritStdio); | |
| } | |
| } | |
| // ignore: unawaited_futures | |
| p.exitCode.then((exitCode) { | |
| if (verbosity >= 0) { | |
| print('Process existed with exitCode $exitCode'); | |
| } | |
| if (!_exited) { | |
| _analyzingCompleter.completeError('Process exited 1.'); | |
| while (_outstandingRequestsWithId.isNotEmpty) { | |
| var entry = _outstandingRequestsWithId.entries.first; | |
| _outstandingRequestsWithId.remove(entry.key); | |
| entry.value.completer.completeError('Process exited 2.'); | |
| } | |
| } | |
| }); | |
| if (verbosity >= 0) { | |
| print('Launched with pid ${p.pid}'); | |
| } | |
| } | |
| void _listenToStdout(List<int> event) { | |
| // General idea taken from | |
| // pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart | |
| for (int element in event) { | |
| _buffer.add(element); | |
| if (verbosity > 3 && | |
| _buffer.length >= 1000 && | |
| _buffer.length % 1000 == 0) { | |
| print( | |
| 'DEBUG MESSAGE: Stdout buffer with length ${_buffer.length} so far: ' | |
| '${utf8.decode(_buffer.view())}', | |
| ); | |
| } | |
| if (_lsp && _headerContentLength == null && _endsWithCrLfCrLf()) { | |
| String headerRaw = utf8.decode(_buffer.view()); | |
| _buffer.clear(); | |
| // Use a regex that makes the '\r' optional to handle "The Dart VM service | |
| // is listening on [..." message - at least on linux - being \n terminated | |
| // which would otherwise mean that we'd be stuck because no message would | |
| // start with 'Content-Length:'. | |
| List<String> headers = headerRaw.split(_newLineRegExp); | |
| for (String header in headers) { | |
| if (!_printedVmServiceStuff && | |
| header.startsWith('The Dart VM service')) { | |
| if (verbosity >= 0) { | |
| print('\n\n$header\n\n'); | |
| } | |
| _printedVmServiceStuff = true; | |
| } | |
| if (header.startsWith('Content-Length:')) { | |
| String contentLength = header | |
| .substring('Content-Length:'.length) | |
| .trim(); | |
| _headerContentLength = int.parse(contentLength); | |
| break; | |
| } | |
| } | |
| } else if ((_lsp && | |
| _headerContentLength != null && | |
| _buffer.length == _headerContentLength!) || | |
| (!_lsp && _endsWithLf())) { | |
| String messageString = utf8.decode(_buffer.view()); | |
| _buffer.clear(); | |
| _headerContentLength = null; | |
| if (messageString.startsWith('The Dart VM service')) { | |
| if (verbosity >= 0) print('\n\n$messageString\n\n'); | |
| continue; | |
| } | |
| if (messageString.startsWith('The Dart DevTools')) { | |
| if (verbosity >= 0) print('\n\n$messageString\n\n'); | |
| continue; | |
| } | |
| Map<String, dynamic> message = | |
| json.decode(messageString) as Map<String, dynamic>; | |
| // For standard LSP progress notifications we need to respond to | |
| // the server calling 'window/workDoneProgress/create' before it will | |
| // send the progress notification. | |
| if (_lsp && message['method'] == r'window/workDoneProgress/create') { | |
| sendNoFlush(LspMessages.response(message['id'], null)); | |
| } | |
| // LSP: {"jsonrpc":"2.0","method":"$/progress","params":{"token":"ANALYZING","value":{"kind":"begin"... | |
| // Analyzer: {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}} | |
| if ((_lsp && | |
| message['method'] == r'$/progress' && | |
| message['params']['token'] == 'ANALYZING') || | |
| (!_lsp && message['event'] == 'server.status')) { | |
| dynamic params = message['params']; | |
| if (params is Map) { | |
| dynamic isAnalyzing = _lsp | |
| ? (params['value']?['kind'] == 'begin') | |
| : params['analysis']?['isAnalyzing']; | |
| if (isAnalyzing is bool) { | |
| latestIsAnalyzing = isAnalyzing; | |
| _analyzingCompleter.complete(isAnalyzing); | |
| _analyzingCompleter = Completer<bool>(); | |
| if (verbosity > 0) { | |
| print('Got analyzerStatus isAnalyzing = $isAnalyzing'); | |
| } | |
| } | |
| } | |
| } | |
| dynamic possibleId = message['id']; | |
| if (!_lsp && possibleId is String && int.tryParse(possibleId) != null) { | |
| possibleId = int.tryParse(possibleId); | |
| } | |
| if (possibleId is int) { | |
| if (possibleId > largestIdSeen) { | |
| largestIdSeen = possibleId; | |
| } | |
| if (verbosity > 0) { | |
| if (messageString.length > 100) { | |
| print('Got message ${messageString.substring(0, 100)}...'); | |
| } else { | |
| print('Got message $messageString'); | |
| } | |
| } | |
| OutstandingRequest? outstandingRequest = _outstandingRequestsWithId | |
| .remove(possibleId); | |
| if (outstandingRequest != null) { | |
| outstandingRequest.stopwatch.stop(); | |
| outstandingRequest.completer.complete(message); | |
| if (verbosity > 2) { | |
| print( | |
| ' => Got response for $possibleId in ' | |
| '${outstandingRequest.stopwatch.elapsed}', | |
| ); | |
| } | |
| } | |
| } else if (verbosity > 1) { | |
| if (messageString.length > 100) { | |
| print('Got message ${messageString.substring(0, 100)}...'); | |
| } else { | |
| print('Got message $messageString'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Future<void> _runLegacy() async { | |
| assert(!_lsp); | |
| await _launch(lsp: false); | |
| p.stdout.listen(_listenToStdout); | |
| p.stderr.listen(stderr.add); | |
| longRunningRequestsTimer = Timer.periodic( | |
| const Duration(seconds: 1), | |
| _checkLongRunningRequests, | |
| ); | |
| await _initializeLegacy(); | |
| if (verbosity >= 0) { | |
| print('Should now be initialized.'); | |
| } | |
| await afterInitialization(); | |
| } | |
| Future<void> _runLsp() async { | |
| assert(_lsp); | |
| await _launch(lsp: true); | |
| p.stdout.listen(_listenToStdout); | |
| p.stderr.listen(stderr.add); | |
| longRunningRequestsTimer = Timer.periodic( | |
| const Duration(seconds: 1), | |
| _checkLongRunningRequests, | |
| ); | |
| await _initializeLsp(); | |
| if (verbosity >= 0) { | |
| print('Should now be initialized.'); | |
| } | |
| await afterInitialization(); | |
| } | |
| } | |
| class DurationInfo { | |
| final String name; | |
| final Duration duration; | |
| new(this.name, this.duration); | |
| } | |
| enum LaunchFrom { source, dart, aot, aotWithPerf } | |
| class MemoryInfo { | |
| final String name; | |
| final int kb; | |
| new(this.name, this.kb); | |
| } | |
| class OutstandingRequest { | |
| final Stopwatch stopwatch = Stopwatch(); | |
| final Completer<Map<String, dynamic>> completer = Completer(); | |
| new() { | |
| stopwatch.start(); | |
| } | |
| } | |
| class _Uint8ListHelper { | |
| Uint8List data; | |
| int length = 0; | |
| new() : data = Uint8List(1024); | |
| int operator [](int index) { | |
| if (index < 0 || index >= length) throw 'Out of bounds: $index'; | |
| return data[index]; | |
| } | |
| void add(int i) { | |
| if (length == data.length) { | |
| var newData = Uint8List(length * 2); | |
| newData.setRange(0, length, data); | |
| data = newData; | |
| } | |
| data[length++] = i; | |
| } | |
| void clear() { | |
| length = 0; | |
| } | |
| Uint8List view() { | |
| return Uint8List.sublistView(data, 0, length); | |
| } | |
| } | |
| extension on List<MemoryInfo> { | |
| void addInfoMaybe(String description, int? value) { | |
| if (value == null) return; | |
| add(MemoryInfo(description, value)); | |
| } | |
| } |