Skip to content

Commit 3873af2

Browse files
jensjohaCommit Queue
authored andcommitted
[analyzer server] Legacy protocol debounces getFixes and getAsssits requests too; benchmark
In reports of the analyzer being slow we've seen `edit.getFixes` causing a long queue because they take longer to execute than the wait before the next one comes in. While we haven't been able to reproduce that, this CL adds a benchmark that fires *a lot* of both `edit.getFixes` (seen in reports from users) and `edit.getAssists` (which seems, locally at least, to happen every time the cursor moves), and debounces them, changing the benchmark results from ``` 4 files / CodeType.ImportCycle: Initial analysis: 1.322030 Completion after change: 3.096128 4 files / CodeType.ImportChain: Initial analysis: 1.361750 Completion after change: 3.500849 4 files / CodeType.ImportExportCycle: Initial analysis: 1.349346 Completion after change: 3.065497 4 files / CodeType.ImportExportChain: Initial analysis: 1.367151 Completion after change: 3.246891 4 files / CodeType.ImportCycleExportChain: Initial analysis: 1.360573 Completion after change: 3.393901 ``` to ``` 4 files / CodeType.ImportCycle: Initial analysis: 1.322070 Completion after change: 0.546532 4 files / CodeType.ImportChain: Initial analysis: 1.410870 Completion after change: 0.649789 4 files / CodeType.ImportExportCycle: Initial analysis: 1.349923 Completion after change: 0.741040 4 files / CodeType.ImportExportChain: Initial analysis: 1.360396 Completion after change: 0.638332 4 files / CodeType.ImportCycleExportChain: Initial analysis: 1.354682 Completion after change: 0.658086 ``` Change-Id: Icb0423133726e02e08e204b1c59209264889f8a6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/413682 Commit-Queue: Jens Johansen <[email protected]> Reviewed-by: Phil Quitslund <[email protected]>
1 parent ad58bc0 commit 3873af2

File tree

6 files changed

+202
-21
lines changed

6 files changed

+202
-21
lines changed

pkg/analysis_server/lib/src/server/debounce_requests.dart

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import 'package:analysis_server/src/channel/channel.dart';
1212
/// Return the stream of requests that is filtered to exclude requests for
1313
/// which the client does not need actual responses.
1414
///
15-
/// If there is one completion request, and then another completion request,
16-
/// then most probably the user continued typing, and there is no need to
17-
/// compute results for the first request. But we will have to respond, an
18-
/// empty response is enough.
19-
/// The same goes for hover requests.
15+
/// If there is a request (for example for completion), and then another request
16+
/// of the same kind, then most probably the user continued typing, and there is
17+
/// no need to compute results for the first request. But we will have to
18+
/// respond, an empty response is enough.
19+
///
20+
/// Debounced requests include:
21+
///
22+
/// * `getAssists`
23+
/// * `getCompletions`
24+
/// * `getFixes`
25+
/// * `getHover`
2026
///
2127
/// Discarded requests are reported into [discardedRequests].
2228
Stream<RequestOrResponse> debounceRequests(
@@ -60,6 +66,8 @@ class _DebounceRequests {
6066
var reversed = <RequestOrResponse>[];
6167
var abortCompletionRequests = false;
6268
var abortHoverRequests = false;
69+
var abortAssistsRequests = false;
70+
var abortFixesRequests = false;
6371
for (var requestOrResponse in requests.reversed) {
6472
if (requestOrResponse is Request) {
6573
if (requestOrResponse.method == ANALYSIS_REQUEST_UPDATE_CONTENT) {
@@ -102,6 +110,36 @@ class _DebounceRequests {
102110
} else {
103111
abortHoverRequests = true;
104112
}
113+
} else if (requestOrResponse.method == EDIT_REQUEST_GET_ASSISTS) {
114+
if (abortAssistsRequests) {
115+
discardedRequests.add(requestOrResponse);
116+
channel.sendResponse(
117+
EditGetAssistsResult([]).toResponse(
118+
requestOrResponse.id,
119+
// We can use a null converter here because we're not sending
120+
// any path.
121+
clientUriConverter: null,
122+
),
123+
);
124+
continue;
125+
} else {
126+
abortAssistsRequests = true;
127+
}
128+
} else if (requestOrResponse.method == EDIT_REQUEST_GET_FIXES) {
129+
if (abortFixesRequests) {
130+
discardedRequests.add(requestOrResponse);
131+
channel.sendResponse(
132+
EditGetFixesResult([]).toResponse(
133+
requestOrResponse.id,
134+
// We can use a null converter here because we're not sending
135+
// any path.
136+
clientUriConverter: null,
137+
),
138+
);
139+
continue;
140+
} else {
141+
abortFixesRequests = true;
142+
}
105143
}
106144
}
107145
reversed.add(requestOrResponse);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import '../language_server_benchmark.dart';
6+
import '../legacy_messages.dart';
7+
import 'utils.dart';
8+
9+
/// In reports of the analyzer being slow we've seen `edit.getFixes` causing a
10+
/// long queue because they take longer to execute than the wait before the next
11+
/// one comes in.
12+
/// While we haven't been able to reproduce that we can surely fire a whole lot
13+
/// of them very fast, being able to meassure that we "debounce" them is we get
14+
/// too many at once.
15+
/// Here we'll fire both `edit.getFixes` (seen in reports from users)
16+
/// and `edit.getAssists` (which seems, locally at least, to happen every time
17+
/// the cursor moves).
18+
Future<void> main() async {
19+
await runHelper(
20+
LegacyManyGetFixesAndGetAssisstRequestsBenchmark.new,
21+
runAsLsp: false,
22+
// The number of files doesn't seem to be important on this one.
23+
numberOfFileOptions: [4],
24+
);
25+
}
26+
27+
class LegacyManyGetFixesAndGetAssisstRequestsBenchmark
28+
extends DartLanguageServerBenchmark {
29+
@override
30+
final Uri rootUri;
31+
@override
32+
final Uri cacheFolder;
33+
34+
final RunDetails runDetails;
35+
36+
LegacyManyGetFixesAndGetAssisstRequestsBenchmark(
37+
this.rootUri,
38+
this.cacheFolder,
39+
this.runDetails,
40+
) : super(useLspProtocol: false);
41+
42+
@override
43+
LaunchFrom get launchFrom => LaunchFrom.Dart;
44+
45+
@override
46+
Future<void> afterInitialization() async {
47+
await send(
48+
LegacyMessages.setPriorityFiles(largestIdSeen + 1, [
49+
runDetails.mainFile.uri,
50+
runDetails.orderedFileCopies.first.uri,
51+
]),
52+
);
53+
54+
// This is probably not realistic, but not being able to reproduce fewer
55+
// requests, each taking longer, for now this is better than nothing.
56+
for (var i = 0; i < 2000; i++) {
57+
await send(
58+
LegacyMessages.getFixes(
59+
largestIdSeen + 1,
60+
runDetails.orderedFileCopies.first.uri,
61+
i,
62+
),
63+
);
64+
await send(
65+
LegacyMessages.getAssists(
66+
largestIdSeen + 1,
67+
runDetails.orderedFileCopies.first.uri,
68+
i,
69+
),
70+
);
71+
}
72+
73+
// Type 'ge' in the empty line in main.
74+
await send(
75+
LegacyMessages.updateContent(
76+
largestIdSeen + 1,
77+
runDetails.mainFile.uri,
78+
runDetails.mainFileTypingContent,
79+
),
80+
);
81+
82+
// Ask for completion
83+
Future<Map<String, dynamic>> completionFuture =
84+
(await send(
85+
LegacyMessages.getSuggestions2(
86+
largestIdSeen + 1,
87+
runDetails.mainFile.uri,
88+
runDetails.typingAtOffset,
89+
),
90+
))!.completer.future;
91+
92+
Stopwatch stopwatch = Stopwatch()..start();
93+
var completionResponse = await completionFuture;
94+
List<dynamic> completionItems =
95+
completionResponse['result']['suggestions'] as List;
96+
var completionAfterChange = stopwatch.elapsed;
97+
durationInfo.add(
98+
DurationInfo('Completion after change', completionAfterChange),
99+
);
100+
print(
101+
'Got ${completionItems.length} completion items '
102+
'in $completionAfterChange',
103+
);
104+
}
105+
}

pkg/analysis_server/tool/benchmark_tools/big_chain_benchmark/legacy_many_hover_requests.dart

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,6 @@ class LegacyManyHoverRequestsBenchmark extends DartLanguageServerBenchmark {
3939

4040
@override
4141
Future<void> afterInitialization() async {
42-
var mainFileLines = runDetails.mainFile.content.split('\n');
43-
if (!mainFileLines[2].startsWith('void main')) throw 'unexpected file data';
44-
if (mainFileLines[3].trim().isNotEmpty) throw 'unexpected file data';
45-
mainFileLines[3] = ' ge';
46-
var newMainFileContent = StringBuffer();
47-
for (int i = 0; i <= 3; i++) {
48-
newMainFileContent.writeln(mainFileLines[i]);
49-
}
50-
var geOffset = newMainFileContent.length - 1;
51-
for (int i = 4; i < mainFileLines.length; i++) {
52-
newMainFileContent.writeln(mainFileLines[i]);
53-
}
54-
5542
await send(
5643
LegacyMessages.setPriorityFiles(largestIdSeen + 1, [
5744
runDetails.mainFile.uri,
@@ -77,7 +64,7 @@ class LegacyManyHoverRequestsBenchmark extends DartLanguageServerBenchmark {
7764
LegacyMessages.updateContent(
7865
largestIdSeen + 1,
7966
runDetails.mainFile.uri,
80-
newMainFileContent.toString(),
67+
runDetails.mainFileTypingContent,
8168
),
8269
);
8370

@@ -87,7 +74,7 @@ class LegacyManyHoverRequestsBenchmark extends DartLanguageServerBenchmark {
8774
LegacyMessages.getSuggestions2(
8875
largestIdSeen + 1,
8976
runDetails.mainFile.uri,
90-
geOffset,
77+
runDetails.typingAtOffset,
9178
),
9279
))!.completer.future;
9380

pkg/analysis_server/tool/benchmark_tools/big_chain_benchmark/utils.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,25 @@ String get$i() {
8484
import '${getFilenameFor(1)}';
8585
8686
void main(List<String> arguments) {
87-
87+
8888
}
8989
""";
9090
File.fromUri(mainFileUri).writeAsStringSync(mainFileContent);
91+
var typing = ' ge';
92+
var mainFileTypingContent = """
93+
import '${getFilenameFor(1)}';
94+
95+
void main(List<String> arguments) {
96+
$typing
97+
}
98+
""";
99+
var typingAtOffset = mainFileTypingContent.indexOf(typing) + typing.length;
91100

92101
return RunDetails(
93102
libDirUri: libDirUri,
94103
mainFile: FileContentPair(mainFileUri, mainFileContent),
104+
mainFileTypingContent: mainFileTypingContent,
105+
typingAtOffset: typingAtOffset,
95106
orderedFileCopies: orderedFileCopies,
96107
numFiles: numFiles,
97108
);
@@ -178,12 +189,16 @@ class FileContentPair {
178189
class RunDetails {
179190
final Uri libDirUri;
180191
final FileContentPair mainFile;
192+
final String mainFileTypingContent;
193+
final int typingAtOffset;
181194
final List<FileContentPair> orderedFileCopies;
182195
final int numFiles;
183196

184197
RunDetails({
185198
required this.libDirUri,
186199
required this.mainFile,
200+
required this.mainFileTypingContent,
201+
required this.typingAtOffset,
187202
required this.orderedFileCopies,
188203
required this.numFiles,
189204
});

pkg/analysis_server/tool/benchmark_tools/language_server_benchmark.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ abstract class DartLanguageServerBenchmark {
8585
print('Sending message with id $possibleId');
8686
}
8787
}
88+
if (verbosity > 3) {
89+
print('Sending message: $jsonEncodedBody');
90+
}
8891

8992
if (_lsp) {
9093
// Header is always ascii, body is always utf8!
@@ -295,6 +298,16 @@ abstract class DartLanguageServerBenchmark {
295298
String messageString = utf8.decode(_buffer);
296299
_buffer.clear();
297300
_headerContentLength = null;
301+
302+
if (messageString.startsWith('The Dart VM service')) {
303+
print('\n\n$messageString\n\n');
304+
continue;
305+
}
306+
if (messageString.startsWith('The Dart DevTools')) {
307+
print('\n\n$messageString\n\n');
308+
continue;
309+
}
310+
298311
Map<String, dynamic> message =
299312
json.decode(messageString) as Map<String, dynamic>;
300313

pkg/analysis_server/tool/benchmark_tools/legacy_messages.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ String _uriToStringWithoutEndingSlash(Uri uri) {
99
}
1010

1111
class LegacyMessages {
12+
static Map<String, dynamic> getAssists(
13+
int id,
14+
Uri file,
15+
int offset, {
16+
int length = 0,
17+
}) {
18+
return {
19+
'id': '$id',
20+
'method': 'edit.getAssists',
21+
'params': {'file': '$file', 'offset': offset, 'length': length},
22+
'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
23+
};
24+
}
25+
26+
static Map<String, dynamic> getFixes(int id, Uri file, int offset) {
27+
return {
28+
'id': '$id',
29+
'method': 'edit.getFixes',
30+
'params': {'file': '$file', 'offset': offset},
31+
'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
32+
};
33+
}
34+
1235
static Map<String, dynamic> getHover(int id, Uri file, int offset) {
1336
return {
1437
'id': '$id',

0 commit comments

Comments
 (0)