blob: 05e63346af10af20f18c235b539c06b537e7aa41 [file] [log] [blame]
// 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;
/// 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;
DartLanguageServerBenchmark(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 {
// 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]);
}
await p.stdin.flush();
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>;
// LSP: {"jsonrpc":"2.0","method":"$/analyzerStatus","params":{"isAnalyzing":false}}
// Analyzer: {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
if ((_lsp && message['method'] == r'$/analyzerStatus') ||
(!_lsp && message['event'] == 'server.status')) {
dynamic params = message['params'];
if (params is Map) {
dynamic isAnalyzing =
_lsp
? params['isAnalyzing']
: params['analysis']?['isAnalyzing'];
if (isAnalyzing is bool) {
_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;
DurationInfo(this.name, this.duration);
}
enum LaunchFrom { Source, Dart, Aot, AotWithPerf }
class MemoryInfo {
final String name;
final int kb;
MemoryInfo(this.name, this.kb);
}
class OutstandingRequest {
final Stopwatch stopwatch = Stopwatch();
final Completer<Map<String, dynamic>> completer = Completer();
OutstandingRequest() {
stopwatch.start();
}
}
class _Uint8ListHelper {
Uint8List data;
int length = 0;
_Uint8ListHelper() : 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));
}
}