// Copyright (c) 2016, 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' show jsonDecode, jsonEncode; import 'dart:io'; import 'package:analysis_server/lsp_protocol/protocol_generated.dart'; import 'package:analysis_server/src/lsp/client_capabilities.dart'; import 'package:analysis_server/src/protocol_server.dart'; import 'package:analyzer/instrumentation/instrumentation.dart'; import 'package:test/test.dart'; import '../../test/integration/lsp_server/integration_tests.dart'; import '../../test/integration/support/integration_tests.dart'; import '../../test/lsp/server_abstract.dart' show ClientCapabilitiesHelperMixin; /// A server protocol-agnostic interface to the memory test, allowing the same /// benchmarks to run for both the original protocol and LSP. abstract class AbstractBenchmarkTest { Future get analysisFinished; Future closeFile(String filePath); Future complete(String filePath, int offset); void debugStdio(); Future getMemoryUsage(); Future openFile(String filePath, String contents); Future setUp(List roots); Future shutdown(); Future updateFile(String filePath, String contents); } /// An implementation of [AbstractBenchmarkTest] for a original protocol memory /// test. class AnalysisServerBenchmarkTest extends AbstractBenchmarkTest { final _test = AnalysisServerMemoryUsageTest(); @override Future get analysisFinished => _test.analysisFinished; @override Future closeFile(String filePath) => _test.sendAnalysisUpdateContent({filePath: RemoveContentOverlay()}); @override Future complete(String filePath, int offset) async { // Create a new non-broadcast stream and subscribe to // test.onCompletionResults before sending a request. // Otherwise we could skip results which where posted to // test.onCompletionResults after request is sent but // before subscribing to test.onCompletionResults. final completionResults = StreamController(); completionResults.sink.addStream(_test.onCompletionResults); var result = await _test.sendCompletionGetSuggestions(filePath, offset); var future = completionResults.stream .where((CompletionResultsParams params) => params.id == result.id && params.isLast) .first; await future; } @override void debugStdio() => _test.debugStdio(); @override Future getMemoryUsage() => _test.getMemoryUsage(); @override Future openFile(String filePath, String contents) async { await _test .sendAnalysisUpdateContent({filePath: AddContentOverlay(contents)}); await _test.sendAnalysisSetPriorityFiles([filePath]); } @override Future setUp(List roots) async { await _test.setUp(); await _test.subscribeToStatusNotifications(); await _test.sendAnalysisSetAnalysisRoots(roots, []); } @override Future shutdown() => _test.shutdown(); @override Future updateFile(String filePath, String contents) => _test.sendAnalysisUpdateContent({filePath: AddContentOverlay(contents)}); } /// Base class for analysis server memory usage tests. class AnalysisServerMemoryUsageTest extends AbstractAnalysisServerIntegrationTest with ServerMemoryUsageMixin { /// Send the server an 'analysis.setAnalysisRoots' command directing it to /// analyze [sourceDirectory]. Future setAnalysisRoot() => sendAnalysisSetAnalysisRoots([sourceDirectory.path], []); /// The server is automatically started before every test. @override Future setUp() async { _vmServicePort = await ServiceProtocol._findAvailableSocketPort(); onAnalysisErrors.listen((AnalysisErrorsParams params) { currentAnalysisErrors[params.file] = params.errors; }); onServerError.listen((ServerErrorParams params) { // A server error should never happen during an integration test. fail('${params.message}\n${params.stackTrace}'); }); var serverConnected = Completer(); onServerConnected.listen((_) { outOfTestExpect(serverConnected.isCompleted, isFalse); serverConnected.complete(); }); return startServer(servicesPort: _vmServicePort).then((_) { server.listenToOutput(dispatchNotification); server.exitCode.then((_) { skipShutdown = true; }); return serverConnected.future; }); } /// After every test, the server is stopped. Future shutdown() async => await shutdownIfNeeded(); /// Enable [ServerService.STATUS] notifications so that [analysisFinished] /// can be used. Future subscribeToStatusNotifications() async { await sendServerSetSubscriptions([ServerService.STATUS]); } } /// An implementation of [AbstractBenchmarkTest] for an LSP memory test. class LspAnalysisServerBenchmarkTest extends AbstractBenchmarkTest with ClientCapabilitiesHelperMixin { final _test = LspAnalysisServerMemoryUsageTest(); final PrintableLogger _logger = PrintableLogger(); /// Track the file contents so we can easily convert offsets (used in /// the interface) to Positions required by LSP without having to keep /// passing in the contents. final Map _fileContents = {}; int _fileVersion = 1; @override Future get analysisFinished => _test.waitForAnalysisComplete(); @override Future closeFile(String filePath) { _fileContents.remove(filePath); return _test.closeFile(Uri.file(filePath)); } @override Future complete(String filePath, int offset) { final contents = _fileContents[filePath]!; final position = _test.positionFromOffset(offset, contents); return _test.getCompletion(Uri.file(filePath), position); } @override void debugStdio() => _logger.debugStdio(); @override Future getMemoryUsage() => _test.getMemoryUsage(); @override Future openFile(String filePath, String contents) { _fileContents[filePath] = contents; return _test.openFile(Uri.file(filePath), contents, version: _fileVersion++); } @override Future setUp(List roots) async { _test.instrumentationService = InstrumentationLogAdapter(_logger); await _test.setUp(); _test.projectFolderPath = roots.single; _test.projectFolderUri = Uri.file(_test.projectFolderPath); // Use some reasonable default client capabilities that will activate // features that will excercise more code that benchmarks should measure // (such as applyEdit to allow suggestion sets results to be merged in). await _test.initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( withCompletionItemKinds( emptyTextDocumentClientCapabilities, LspClientCapabilities.defaultSupportedCompletionKinds.toList(), ), ), workspaceCapabilities: withDocumentChangesSupport( withApplyEditSupport(emptyWorkspaceClientCapabilities), ), windowCapabilities: withWorkDoneProgressSupport(emptyWindowClientCapabilities), ); } @override Future shutdown() async { _test.tearDown(); _logger.shutdown(); } @override Future updateFile(String filePath, String contents) { _fileContents[filePath] = contents; return _test.replaceFile(_fileVersion++, Uri.file(filePath), contents); } } /// Base class for LSP analysis server memory usage tests. class LspAnalysisServerMemoryUsageTest extends AbstractLspAnalysisServerIntegrationTest with ServerMemoryUsageMixin { Map> currentAnalysisErrors = {}; @override void expect(actual, matcher, {String? reason}) => outOfTestExpect(actual, matcher, reason: reason); /// The server is automatically started before every test. @override Future setUp() async { _vmServicePort = await ServiceProtocol._findAvailableSocketPort(); vmArgs.addAll([ '--enable-vm-service=$_vmServicePort', '-DSILENT_OBSERVATORY=true', '--disable-service-auth-codes', '--disable-dart-dev' ]); await super.setUp(); errorNotificationsFromServer.listen((NotificationMessage error) { // A server error should never happen during an integration test. fail('${error.toJson()}'); }); } /// After every test, the server is stopped. Future shutdown() async => this.tearDown(); } mixin ServerMemoryUsageMixin { late int _vmServicePort; Future getMemoryUsage() async { var uri = Uri.parse('ws://127.0.0.1:$_vmServicePort/ws'); var service = await ServiceProtocol.connect(uri); var vm = await service.call('getVM'); var total = 0; List isolateRefs = vm['isolates']; for (Map isolateRef in isolateRefs) { var isolate = await service.call('getIsolate', {'isolateId': isolateRef['id']}); Map _heaps = isolate['_heaps']; total += _heaps['new']['used'] + _heaps['new']['external'] as int; total += _heaps['old']['used'] + _heaps['old']['external'] as int; } service.dispose(); return total; } } class ServiceProtocol { final WebSocket socket; int _id = 0; final Map> _completers = {}; ServiceProtocol._(this.socket) { socket.listen(_handleMessage); } Future call(String method, [Map args = const {}]) { var id = '${++_id}'; var completer = Completer(); _completers[id] = completer; var m = { 'jsonrpc': '2.0', 'id': id, 'method': method, 'args': args }; m['params'] = args; var message = jsonEncode(m); socket.add(message); return completer.future; } Future dispose() => socket.close(); void _handleMessage(dynamic message) { if (message is! String) { return; } try { dynamic json = jsonDecode(message); if (json.containsKey('id')) { dynamic id = json['id']; _completers[id]?.complete(json['result']); _completers.remove(id); } } catch (e) { // ignore } } static Future connect(Uri uri) async { var socket = await WebSocket.connect(uri.toString()); return ServiceProtocol._(socket); } static Future _findAvailableSocketPort() async { var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); try { return socket.port; } finally { await socket.close(); } } }