[analysis_server] Record timings for recent LSP requests and show in server diagnostics

Change-Id: I1bfb03faa9ce3240c700c93cb4ab677dc1aff520
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243820
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2022-05-05 15:29:55 +00:00 committed by Commit Bot
parent 7d6216940f
commit 5f15aefcab
13 changed files with 283 additions and 243 deletions

View file

@ -285,3 +285,14 @@ class LspJsonHandler<T> {
abstract class ToJsonable {
Object toJson();
}
extension IncomingMessageExtension on IncomingMessage {
/// Returns the amount of time (in milliseconds) since the client sent this
/// request or `null` if the client did not provide [clientRequestTime].
int? get timeSinceRequest {
var clientRequestTime = this.clientRequestTime;
return clientRequestTime != null
? DateTime.now().millisecondsSinceEpoch - clientRequestTime
: null;
}
}

View file

@ -14,6 +14,8 @@ import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/plugin/plugin_watcher.dart';
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/server/performance.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
@ -124,6 +126,9 @@ abstract class AbstractAnalysisServer {
/// Performance information before initial analysis is complete.
final ServerPerformance performanceDuringStartup = ServerPerformance();
/// Performance about recent requests.
final ServerRecentPerformance recentPerformance = ServerRecentPerformance();
RequestStatisticsHelper? requestStatistics;
PerformanceLog? analysisPerformanceLogger;
@ -543,3 +548,18 @@ abstract class AbstractAnalysisServer {
return null;
}
}
class ServerRecentPerformance {
/// The maximum number of performance measurements to keep.
static const int performanceListMaxLength = 50;
/// A list of code completion performance measurements for the latest
/// completion operation up to [performanceListMaxLength] measurements.
final RecentBuffer<CompletionPerformance> completion =
RecentBuffer<CompletionPerformance>(performanceListMaxLength);
/// A [RecentBuffer] for performance information about the most recent
/// requests.
final RecentBuffer<RequestPerformance> requests =
RecentBuffer(performanceListMaxLength);
}

View file

@ -93,13 +93,13 @@ class CompletionGetSuggestionsHandler extends CompletionGetSuggestions2Handler {
}
final completionPerformance = CompletionPerformance(
operation: performance,
performance: performance,
path: file,
requestLatency: requestLatency,
content: resolvedUnit.content,
offset: offset,
);
server.completionState.performanceList.add(completionPerformance);
server.recentPerformance.completion.add(completionPerformance);
var declarationsTracker = server.declarationsTracker;
if (declarationsTracker == null) {

View file

@ -176,13 +176,13 @@ class CompletionGetSuggestions2Handler extends CompletionHandler
}
final completionPerformance = CompletionPerformance(
operation: performance,
performance: performance,
path: file,
requestLatency: requestLatency,
content: resolvedUnit.content,
offset: offset,
);
server.completionState.performanceList.add(completionPerformance);
server.recentPerformance.completion.add(completionPerformance);
var analysisSession = resolvedUnit.analysisSession;
var enclosingNode =

View file

@ -115,14 +115,14 @@ class CompletionHandler extends MessageHandler<CompletionParams, CompletionList>
'request',
(performance) async {
final thisPerformance = CompletionPerformance(
operation: performance,
performance: performance,
path: result.path,
requestLatency: requestLatency,
content: result.content,
offset: offset,
);
completionPerformance = thisPerformance;
server.performanceStats.completion.add(thisPerformance);
server.recentPerformance.completion.add(thisPerformance);
// `await` required for `performance.runAsync` to count time.
return await _getServerDartItems(

View file

@ -183,7 +183,7 @@ abstract class MessageHandler<P, R>
final params =
paramsJson != null ? jsonHandler.convertParams(paramsJson) : null as P;
final messageInfo = MessageInfo(message.clientRequestTime);
final messageInfo = MessageInfo(timeSinceRequest: message.timeSinceRequest);
return handle(params, messageInfo, token);
}
}
@ -191,18 +191,11 @@ abstract class MessageHandler<P, R>
/// Additional information about an incoming message (request or notification)
/// provided to a handler.
class MessageInfo {
final int? clientRequestTime;
MessageInfo(this.clientRequestTime);
/// Returns the amount of time (in milliseconds) since the client sent this
/// request or `null` if the client did not provide [clientRequestTime].
int? get timeSinceRequest {
var clientRequestTime = this.clientRequestTime;
return clientRequestTime != null
? DateTime.now().millisecondsSinceEpoch - clientRequestTime
: null;
}
final int? timeSinceRequest;
MessageInfo({this.timeSinceRequest});
}
/// A message handler that handles all messages for a given server state.

View file

@ -9,7 +9,6 @@ import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/analysis_server_abstract.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/computer/computer_closingLabels.dart';
import 'package:analysis_server/src/computer/computer_outline.dart';
import 'package:analysis_server/src/context_manager.dart';
@ -30,9 +29,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/server/error_notifier.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart'
show CompletionPerformance;
import 'package:analysis_server/src/services/completion/completion_state.dart';
import 'package:analysis_server/src/server/performance.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/utilities/process.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
@ -47,6 +44,7 @@ import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
@ -99,8 +97,6 @@ class LspAnalysisServer extends AbstractAnalysisServer {
ServerCapabilities? capabilities;
late ServerCapabilitiesComputer capabilitiesComputer;
LspPerformance performanceStats = LspPerformance();
/// Whether or not the server is controlling the shutdown and will exit
/// automatically.
bool willExit = false;
@ -355,15 +351,25 @@ class LspAnalysisServer extends AbstractAnalysisServer {
if (message is ResponseMessage) {
handleClientResponse(message);
} else if (message is RequestMessage) {
final result = await messageHandler.handleMessage(message);
if (result.isError) {
sendErrorResponse(message, result.error);
} else {
channel.sendResponse(ResponseMessage(
id: message.id,
result: result.result,
jsonrpc: jsonRpcVersion));
}
// Record performance information for the request.
final performance = OperationPerformanceImpl('<root>');
await performance.runAsync('request', (performance) async {
final requestPerformance = RequestPerformance(
operation: message.method.toString(),
performance: performance,
requestLatency: message.timeSinceRequest,
);
recentPerformance.requests.add(requestPerformance);
final result = await messageHandler.handleMessage(message);
if (result.isError) {
sendErrorResponse(message, result.error);
} else {
channel.sendResponse(ResponseMessage(
id: message.id,
result: result.result,
jsonrpc: jsonRpcVersion));
}
});
} else if (message is NotificationMessage) {
final result = await messageHandler.handleMessage(message);
if (result.isError) {
@ -857,14 +863,6 @@ class LspInitializationOptions {
flutterOutline = options != null && options['flutterOutline'] == true;
}
class LspPerformance {
/// A list of code completion performance measurements for the latest
/// completion operation up to [performanceListMaxLength] measurements.
final RecentBuffer<CompletionPerformance> completion =
RecentBuffer<CompletionPerformance>(
CompletionState.performanceListMaxLength);
}
class LspServerContextManagerCallbacks extends ContextManagerCallbacks {
// TODO(dantup): Lots of copy/paste from the Analysis Server one here.

View file

@ -0,0 +1,19 @@
// Copyright (c) 2022, 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 'package:analyzer/src/util/performance/operation_performance.dart';
class RequestPerformance {
static var _nextId = 1;
final int id;
final OperationPerformance performance;
final int? requestLatency;
final String operation;
RequestPerformance({
required this.operation,
required this.performance,
this.requestLatency,
}) : id = _nextId++;
}

View file

@ -2,7 +2,7 @@
// 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 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analysis_server/src/server/performance.dart';
/// Compute a string representing a code completion operation at the
/// given source and location.
@ -34,24 +34,20 @@ String _computeCompletionSnippet(String contents, int offset) {
}
/// Overall performance of a code completion operation.
class CompletionPerformance {
static var _nextId = 1;
final int id;
final OperationPerformance operation;
class CompletionPerformance extends RequestPerformance {
final String path;
final String snippet;
final int? requestLatency;
int? computedSuggestionCount;
int? transmittedSuggestionCount;
CompletionPerformance({
required this.operation,
required super.performance,
required this.path,
this.requestLatency,
super.requestLatency,
required String content,
required int offset,
}) : id = _nextId++,
snippet = _computeCompletionSnippet(content, offset);
}) : snippet = _computeCompletionSnippet(content, offset),
super(operation: 'Completion');
String get computedSuggestionCountStr {
if (computedSuggestionCount == null) return '';
@ -59,7 +55,7 @@ class CompletionPerformance {
}
int get elapsedInMilliseconds {
return operation.elapsed.inMilliseconds;
return performance.elapsed.inMilliseconds;
}
String get transmittedSuggestionCountStr {

View file

@ -3,14 +3,9 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
class CompletionState {
/// The maximum number of performance measurements to keep.
static const int performanceListMaxLength = 50;
/// The time budget for a completion request.
Duration budgetDuration = CompletionBudget.defaultDuration;
@ -20,11 +15,6 @@ class CompletionState {
/// The next completion response id.
int nextCompletionId = 0;
/// A list of code completion performance measurements for the latest
/// completion operation up to [performanceListMaxLength] measurements.
final RecentBuffer<CompletionPerformance> performanceList =
RecentBuffer<CompletionPerformance>(performanceListMaxLength);
/// The current request being processed or `null` if none.
DartCompletionRequest? currentRequest;
}

View file

@ -14,6 +14,7 @@ import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'
show LspAnalysisServer;
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/server/http_server.dart';
import 'package:analysis_server/src/server/performance.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/src/status/ast_writer.dart';
@ -156,136 +157,6 @@ String writeOption(String name, dynamic value) {
return '$name: <code>$value</code><br> ';
}
abstract class AbstractCompletionPage extends DiagnosticPageWithNav {
AbstractCompletionPage(DiagnosticsSite site)
: super(site, 'completion', 'Code Completion',
description: 'Latency statistics for code completion.');
path.Context get pathContext;
List<CompletionPerformance> get performanceItems;
@override
Future generateContent(Map<String, String> params) async {
var completions = performanceItems;
if (completions.isEmpty) {
blankslate('No completions recorded.');
return;
}
var fastCount =
completions.where((c) => c.elapsedInMilliseconds <= 100).length;
p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');
// draw a chart
buf.writeln(
'<div id="chart-div" style="width: 700px; height: 300px;"></div>');
var rowData = StringBuffer();
for (var i = completions.length - 1; i >= 0; i--) {
if (rowData.isNotEmpty) {
rowData.write(',');
}
var latency = completions[i].requestLatency ?? 0;
var completionTime = completions[i].elapsedInMilliseconds;
// label, latency, time
// [' ', 21.0, 101.5]
rowData.write("[' ', $latency, $completionTime]");
}
buf.writeln('''
<script type="text/javascript">
google.charts.load('current', {'packages':['bar']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable([
[ 'Completion', 'Latency', 'Time' ],
$rowData
]);
var options = {
bars: 'vertical',
vAxis: {format: 'decimal'},
height: 300,
isStacked: true,
series: {
0: { color: '#C0C0C0' },
1: { color: '#4285f4' },
}
};
var chart = new google.charts.Bar(document.getElementById('chart-div'));
chart.draw(data, google.charts.Bar.convertOptions(options));
}
</script>
''');
// emit the data as a table
buf.writeln('<table>');
buf.writeln(
'<tr><th>Time</th><th>Computed Results</th><th>Transmitted Results</th><th>Source</th><th>Snippet</th></tr>');
for (var completion in completions) {
var shortName = pathContext.basename(completion.path);
buf.writeln('<tr>'
'<td class="pre right"><a href="/timing?id=${completion.id}">'
'${_formatTiming(completion)}'
'</a></td>'
'<td class="right">${completion.computedSuggestionCountStr}</td>'
'<td class="right">${completion.transmittedSuggestionCountStr}</td>'
'<td>${escape(shortName)}</td>'
'<td><code>${escape(completion.snippet)}</code></td>'
'</tr>');
}
buf.writeln('</table>');
}
String _formatTiming(CompletionPerformance completion) {
var buffer = StringBuffer();
buffer.write(printMilliseconds(completion.elapsedInMilliseconds));
var latency = completion.requestLatency;
if (latency != null) {
buffer
..write(' <small class="subtle" title="client-to-server latency">(+ ')
..write(printMilliseconds(latency))
..write(')</small>');
}
return buffer.toString();
}
}
abstract class AbstractCompletionTimingPage extends DiagnosticPageWithNav {
AbstractCompletionTimingPage(DiagnosticsSite site)
: super(site, 'timing', 'Timing', description: 'Timing statistics.');
path.Context get pathContext;
List<CompletionPerformance> get performanceItems;
@override
bool get showInNav => false;
@override
Future generateContent(Map<String, String> params) async {
var id = int.parse(params['id'] ?? '');
var completionInfo =
performanceItems.firstWhereOrNull((info) => info.id == id);
if (completionInfo == null) {
blankslate('Unable to find completion data for $id. '
'Perhaps newer completion requests have pushed it out of the buffer?');
return;
}
var buffer = StringBuffer();
completionInfo.operation.write(buffer: buffer);
pre(() {
buf.write('<code>');
buf.write(escape('$buffer'));
buf.writeln('</code>');
});
return;
}
}
class AstPage extends DiagnosticPageWithNav {
String? _description;
@ -416,32 +287,66 @@ class CommunicationsPage extends DiagnosticPageWithNav {
}
}
class CompletionPage extends AbstractCompletionPage {
@override
AnalysisServer server;
class CompletionPage extends DiagnosticPageWithNav with PerformanceChartMixin {
CompletionPage(DiagnosticsSite site)
: super(site, 'completion', 'Code Completion',
description: 'Latency statistics for code completion.');
CompletionPage(super.site, this.server);
@override
path.Context get pathContext => server.resourceProvider.pathContext;
@override
List<CompletionPerformance> get performanceItems =>
server.completionState.performanceList.items.toList();
}
class CompletionTimingPage extends AbstractCompletionTimingPage {
@override
AnalysisServer server;
CompletionTimingPage(super.site, this.server);
server.recentPerformance.completion.items.toList();
@override
path.Context get pathContext => server.resourceProvider.pathContext;
Future generateContent(Map<String, String> params) async {
var completions = performanceItems;
@override
List<CompletionPerformance> get performanceItems =>
server.completionState.performanceList.items.toList();
if (completions.isEmpty) {
blankslate('No completions recorded.');
return;
}
var fastCount =
completions.where((c) => c.elapsedInMilliseconds <= 100).length;
p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');
drawChart(completions);
// emit the data as a table
buf.writeln('<table>');
buf.writeln(
'<tr><th>Time</th><th>Computed Results</th><th>Transmitted Results</th><th>Source</th><th>Snippet</th></tr>');
for (var completion in completions) {
var shortName = pathContext.basename(completion.path);
buf.writeln(
'<tr>'
'<td class="pre right"><a href="/timing?id=${completion.id}&kind=completion">'
'${_formatTiming(completion)}'
'</a></td>'
'<td class="right">${completion.computedSuggestionCountStr}</td>'
'<td class="right">${completion.transmittedSuggestionCountStr}</td>'
'<td>${escape(shortName)}</td>'
'<td><code>${escape(completion.snippet)}</code></td>'
'</tr>',
);
}
buf.writeln('</table>');
}
String _formatTiming(CompletionPerformance completion) {
var buffer = StringBuffer();
buffer.write(printMilliseconds(completion.elapsedInMilliseconds));
var latency = completion.requestLatency;
if (latency != null) {
buffer
..write(' <small class="subtle" title="client-to-server latency">(+ ')
..write(printMilliseconds(latency))
..write(')</small>');
}
return buffer.toString();
}
}
class ContentsPage extends DiagnosticPageWithNav {
@ -844,15 +749,13 @@ class DiagnosticsSite extends Site implements AbstractGetHandler {
if (server != null) {
pages.add(PluginsPage(this, server));
}
pages.add(CompletionPage(this));
if (server is AnalysisServer) {
pages.add(CompletionPage(this, server));
pages.add(CompletionTimingPage(this, server));
pages.add(SubscriptionsPage(this, server));
} else if (server is LspAnalysisServer) {
pages.add(LspCompletionPage(this, server));
pages.add(LspCompletionTimingPage(this, server));
pages.add(LspCapabilitiesPage(this, server));
}
pages.add(TimingPage(this));
var profiler = ProcessProfiler.getProfilerForPlatform();
if (profiler != null) {
@ -1110,34 +1013,6 @@ class LspCapabilitiesPage extends DiagnosticPageWithNav {
// }
// }
class LspCompletionPage extends AbstractCompletionPage {
@override
LspAnalysisServer server;
LspCompletionPage(super.site, this.server);
@override
path.Context get pathContext => server.resourceProvider.pathContext;
@override
List<CompletionPerformance> get performanceItems =>
server.performanceStats.completion.items.toList();
}
class LspCompletionTimingPage extends AbstractCompletionTimingPage {
@override
LspAnalysisServer server;
LspCompletionTimingPage(super.site, this.server);
@override
path.Context get pathContext => server.resourceProvider.pathContext;
@override
List<CompletionPerformance> get performanceItems =>
server.performanceStats.completion.items.toList();
}
class MemoryAndCpuPage extends DiagnosticPageWithNav {
final ProcessProfiler profiler;
@ -1255,7 +1130,7 @@ class PluginsPage extends DiagnosticPageWithNav {
var requestName = entry.key;
var data = entry.value;
// TODO(brianwilkerson) Consider displaying these times as a graph,
// similar to the one in AbstractCompletionPage.generateContent.
// similar to the one in CompletionPage.generateContent.
var buffer = StringBuffer();
buffer.write(requestName);
buffer.write(' ');
@ -1356,3 +1231,84 @@ class SubscriptionsPage extends DiagnosticPageWithNav {
});
}
}
class TimingPage extends DiagnosticPageWithNav with PerformanceChartMixin {
TimingPage(DiagnosticsSite site)
: super(site, 'timing', 'Timing', description: 'Timing statistics.');
@override
Future generateContent(Map<String, String> params) async {
var kind = params['kind'];
List<RequestPerformance> items;
if (kind == 'completion') {
items = server.recentPerformance.completion.items.toList();
} else {
items = server.recentPerformance.requests.items.toList();
}
var id = int.tryParse(params['id'] ?? '');
if (id == null) {
return _generateList(items);
} else {
return _generateDetails(id, items);
}
}
String _formatTiming(RequestPerformance item) {
var buffer = StringBuffer();
buffer.write(printMilliseconds(item.performance.elapsed.inMilliseconds));
var latency = item.requestLatency;
if (latency != null) {
buffer
..write(' <small class="subtle" title="client-to-server latency">(+ ')
..write(printMilliseconds(latency))
..write(')</small>');
}
return buffer.toString();
}
void _generateDetails(int id, List<RequestPerformance> items) {
var item = items.firstWhereOrNull((info) => info.id == id);
if (item == null) {
blankslate('Unable to find data for $id. '
'Perhaps newer requests have pushed it out of the buffer?');
return;
}
var buffer = StringBuffer();
item.performance.write(buffer: buffer);
pre(() {
buf.write('<code>');
buf.write(escape('$buffer'));
buf.writeln('</code>');
});
}
void _generateList(List<RequestPerformance> items) {
if (items.isEmpty) {
blankslate('No requests recorded.');
return;
}
drawChart(items);
// emit the data as a table
buf.writeln('<table>');
buf.writeln('<tr><th>Time</th><th>Request</th></tr>');
for (var item in items) {
buf.writeln(
'<tr>'
'<td class="pre right"><a href="/timing?id=${item.id}">'
'${_formatTiming(item)}'
'</a></td>'
'<td>${escape(item.operation)}</td>'
'</tr>',
);
}
buf.writeln('</table>');
}
}

View file

@ -5,6 +5,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:analysis_server/src/server/performance.dart';
String escape(String? text) => text == null ? '' : htmlEscape.convert(text);
String printMilliseconds(int value) => '$value ms';
@ -130,6 +132,48 @@ abstract class Page {
}
}
mixin PerformanceChartMixin on Page {
void drawChart(List<RequestPerformance> items) {
buf.writeln(
'<div id="chart-div" style="width: 700px; height: 300px; padding-bottom: 30px;"></div>');
var rowData = StringBuffer();
for (var i = items.length - 1; i >= 0; i--) {
if (rowData.isNotEmpty) {
rowData.write(',');
}
var latency = items[i].requestLatency ?? 0;
var time = items[i].performance.elapsed.inMilliseconds;
// label, latency, time
// [' ', 21.0, 101.5]
rowData.write("[' ', $latency, $time]");
}
buf.writeln('''
<script type="text/javascript">
google.charts.load('current', {'packages':['bar']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable([
[ 'Request', 'Latency', 'Time' ],
$rowData
]);
var options = {
bars: 'vertical',
vAxis: {format: 'decimal'},
height: 300,
isStacked: true,
series: {
0: { color: '#C0C0C0' },
1: { color: '#4285f4' },
}
};
var chart = new google.charts.Bar(document.getElementById('chart-div'));
chart.draw(data, google.charts.Bar.convertOptions(options));
}
</script>
''');
}
}
/// Contains a collection of Pages.
abstract class Site {
final String title;

View file

@ -36,6 +36,19 @@ class ServerTest extends AbstractLspAnalysisServerTest {
expect(server.performanceDuringStartup.latencyCount, isPositive);
}
Future<void> test_capturesRequestPerformance() async {
await initialize(includeClientRequestTime: true);
await openFile(mainFileUri, '');
await expectLater(
getHover(mainFileUri, startOfDocPos),
completes,
);
final performanceItems = server.recentPerformance.requests.items;
final hoverItems = performanceItems.where(
(item) => item.operation == Method.textDocument_hover.toString());
expect(hoverItems, hasLength(1));
}
Future<void> test_inconsistentStateError() async {
await initialize(
// Error is expected and checked below.