Skip to content

Commit f6fcd37

Browse files
jensjohaCommit Queue
authored andcommitted
[analyzer] Add benchmark with plugin that times out
TL;DR: Add benchmark that shows the Dart 3.7 (https://dart-review.googlesource.com/c/sdk/+/386581 specifically) introduced a regression. Details: We've observed that sometimes the plugin that users has installed times out (takes > 500 ms to answer). This benchmark simulates that and shows the worse handling of this introduced in Dart 3.7. Running 10 iterations each of lsp_with_plugin_that_times_out.dart with params 10, ImportChain: Comparing 3.5.4 with 3.6.2 ``` Initial analysis: -7.1855% +/- 2.9168% (-0.09 +/- 0.04) peak virtual memory size: 11.8795% +/- 1.6185% (313.00 +/- 42.64) total program size (virtual): 12.4293% +/- 1.2818% (323.10 +/- 33.32) peak resident set size ("high water mark"): -7.4081% +/- 0.9050% (-38.90 +/- 4.75) size of memory portions (rss): -6.3154% +/- 1.3641% (-32.00 +/- 6.91) ``` I'll note that this is only 10 files and we probably shouldn't put too much weight on the initial analysis time here. Equivalently memory probably isn't super interesting. Comparing 3.6.2 with 3.7.2 ``` Initial analysis: -45.8697% +/- 2.7193% (-0.53 +/- 0.03) Completion call 1: 652.8573% +/- 1.3303% (0.97 +/- 0.00) codeAction call 1: -0.7637% +/- 0.5174% (-0.01 +/- 0.01) Completion call 2: 807.2503% +/- 0.7116% (0.89 +/- 0.00) codeAction call 2: 0.2624% +/- 0.0456% (0.00 +/- 0.00) Completion call 3: 868.9361% +/- 0.3199% (0.90 +/- 0.00) codeAction call 3: 0.1433% +/- 0.0287% (0.00 +/- 0.00) peak virtual memory size: -7.1307% +/- 3.4799% (-210.20 +/- 102.58) total program size (virtual): -8.1674% +/- 3.5673% (-238.70 +/- 104.26) peak resident set size ("high water mark"): -1.2546% +/- 0.7151% (-6.10 +/- 3.48) size of memory portions (rss): -7.6891% +/- 0.9209% (-36.50 +/- 4.37) ``` Again I'll note that this is only 10 files and we probably shouldn't put too much weight on the initial analysis time here - although we've seen this improve in other benchmarks too so I do believe we have an improvement here. And again memory probably isn't super interesting because of the few files. The codeAction calls are basically the same which makes sense: In all cases it's dominated by the plugin that times out. The codeAction call does two calls to the plugin and it takes around 1000 ms (a timeout of 500 ms on each plugin call). Here we see the regression in 3.7 clearly though (cut-out from above): ``` Completion call 1: 652.8573% +/- 1.3303% (0.97 +/- 0.00) Completion call 2: 807.2503% +/- 0.7116% (0.89 +/- 0.00) Completion call 3: 868.9361% +/- 0.3199% (0.90 +/- 0.00) ``` Because of https://dart-review.googlesource.com/c/sdk/+/386581 we only do one request at a time. Before we would interleave the requests, something like this: ``` Request 1: codeAction: |--|waiting for plugin|--|waiting for plugin|--| Request 2: completion: |..--| ``` (I'm trying to ascii-draw that when the codeAction is just awaiting the plugin the completion request can be executed) Now instead we're doing this: ``` Request 1: codeAction: |--|waiting for plugin|--|waiting for plugin|--| Request 2: completion: |..............................................--| ``` (I'm trying to ascii-draw that when the codeAction has to finish before the completion request can be executed) I'll also note that while it's here shown trough a plugin that times out the same will likely happen in other circumstances as well. E.g. the completion resolves with a special call `resolveForCompletion` which in `performWork` gets priority over other things - but when not interleaving requests that has very little effect. For good measure, comparing 3.7.2 with HEAD ``` Completion call 1: 0.6851% +/- 0.2074% (0.01 +/- 0.00) codeAction call 1: 0.6962% +/- 0.1876% (0.01 +/- 0.00) peak virtual memory size: -11.7293% +/- 4.7947% (-321.10 +/- 131.26) total program size (virtual): -12.1763% +/- 4.9125% (-326.80 +/- 131.85) peak resident set size ("high water mark"): -14.7469% +/- 0.6980% (-70.80 +/- 3.35) size of memory portions (rss): -10.4518% +/- 0.8927% (-45.80 +/- 3.91) ``` Not a whole lot of change for the time-related things on this benchmark. Running legacy_with_plugin_that_times_out.dart shows the same thing (here there's only 1 plugin call instead of 2 though; here only looking at completion calls): ``` Comparing 3.5.4 with 3.6.2 Completion call 1: -6.1695% +/- 3.6147% (-0.03 +/- 0.02) Completion call 2: -14.4462% +/- 8.8828% (-0.02 +/- 0.01) Completion call 3: -16.7707% +/- 7.5412% (-0.03 +/- 0.01) Comparing 3.6.2 with 3.7.2 Completion call 1: 139.2436% +/- 3.8814% (0.70 +/- 0.02) Completion call 2: 1128.6580% +/- 14.8207% (1.41 +/- 0.02) Completion call 3: 589.1425% +/- 11.9186% (0.78 +/- 0.02) Comparing 3.7.2 with HEAD no change on completion calls. ``` For fun, lets try to comment out `await completer.future;` in `pkg/analysis_server/lib/src/server/message_scheduler.dart` that was introduced in https://dart-review.googlesource.com/c/sdk/+/386581 (again only looking at completion calls): legacy_with_plugin_that_times_out.dart: ``` Comparing 3.7.2 with HEAD (with commented out await) Completion call 1: -58.5720% +/- 1.9196% (-0.70 +/- 0.02) Completion call 2: -82.8149% +/- 1.1050% (-1.27 +/- 0.02) Completion call 3: -85.6439% +/- 1.5128% (-0.79 +/- 0.01) ``` If looking at the values instead of the percent it looks to almost undo the change from 3.6.2 to 3.7.2, so let's for good measure compare 3.6.2 with that: ``` Comparing 3.6.2 with HEAD (with commented out await) Completion call 2: 95.7654% +/- 11.4279% (0.12 +/- 0.01) ``` So one of the calls is slower, but just double, not 10x. I haven't looked into why. And for lsp_with_plugin_that_times_out.dart: ``` Comparing 3.7.2 with HEAD (with commented out await) Completion call 1: -92.9578% +/- 0.5505% (-1.04 +/- 0.01) Completion call 2: -99.6372% +/- 0.0534% (-1.00 +/- 0.00) Completion call 3: -99.6370% +/- 0.0429% (-1.00 +/- 0.00) ``` The 1000 ms wait time is gone. And again let's for good measure compare 3.6.2 with that: ``` Comparing 3.6.2 with HEAD (with commented out await) Completion call 1: -45.4578% +/- 2.4320% (-0.07 +/- 0.00) Completion call 2: -96.6303% +/- 0.2481% (-0.11 +/- 0.00) Completion call 3: -96.2054% +/- 0.3875% (-0.10 +/- 0.00) ``` Here all the completion calls are faster. I haven't looked into why. Change-Id: I7c312f77b51bb4df68eedeb9bc6e27c2b0175cbf Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/418263 Commit-Queue: Jens Johansen <[email protected]> Reviewed-by: Phil Quitslund <[email protected]>
1 parent fdc4697 commit f6fcd37

File tree

7 files changed

+422
-14
lines changed

7 files changed

+422
-14
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 'dart:isolate';
6+
7+
void main(List<String> args, SendPort sendPort) {
8+
var receivePort = ReceivePort();
9+
sendPort.send(receivePort.sendPort);
10+
receivePort.listen((data) async {
11+
var requestTime = DateTime.now().millisecondsSinceEpoch;
12+
var requestMap = data as Map<String, Object>;
13+
var id = requestMap["id"];
14+
var method = requestMap["method"];
15+
16+
if (method == "plugin.versionCheck") {
17+
var jsonObject = <String, Object>{};
18+
jsonObject["id"] = id!;
19+
jsonObject["requestTime"] = requestTime;
20+
Map<String, Object> resultData = {};
21+
resultData["isCompatible"] = true;
22+
resultData["name"] = "benchmark_helper_plugin";
23+
resultData["version"] = "0.0.1";
24+
resultData["interestingFiles"] = ["**/*.dart"];
25+
jsonObject["result"] = resultData;
26+
sendPort.send(jsonObject);
27+
} else {
28+
await Future.delayed(const Duration(seconds: 1));
29+
var jsonObject = <String, Object>{};
30+
jsonObject["id"] = id!;
31+
jsonObject["error"] = "too slow working on $method";
32+
jsonObject["requestTime"] = requestTime;
33+
sendPort.send(jsonObject);
34+
}
35+
});
36+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
/// We've observed that sometimes the plugin that users has installed times out
10+
/// (takes > 500 ms to answer). This benchmark simulates that and shows the
11+
/// worse handling of this introduced in Dart 3.7.
12+
Future<void> main(List<String> args) async {
13+
await runHelper(
14+
args,
15+
LegacyWithPluginThatTimesOutBencmark.new,
16+
runAsLsp: false,
17+
includePlugin: true,
18+
// The number of files isn't important here.
19+
numberOfFileOptions: [10],
20+
);
21+
}
22+
23+
class LegacyWithPluginThatTimesOutBencmark extends DartLanguageServerBenchmark {
24+
@override
25+
final Uri rootUri;
26+
@override
27+
final Uri cacheFolder;
28+
29+
final RunDetails runDetails;
30+
31+
LegacyWithPluginThatTimesOutBencmark(
32+
super.args,
33+
this.rootUri,
34+
this.cacheFolder,
35+
this.runDetails,
36+
) : super(useLspProtocol: false);
37+
38+
@override
39+
LaunchFrom get launchFrom => LaunchFrom.Dart;
40+
41+
@override
42+
Future<void> afterInitialization() async {
43+
// The main file is an open tab.
44+
await send(
45+
LegacyMessages.setPriorityFiles(largestIdSeen + 1, [
46+
runDetails.mainFile.uri,
47+
]),
48+
);
49+
50+
// Does this change anything?
51+
await Future.delayed(const Duration(milliseconds: 100));
52+
53+
// The user starts typing - types 'ge' in the empty line in main...
54+
await send(
55+
LegacyMessages.updateContent(
56+
largestIdSeen + 1,
57+
runDetails.mainFile.uri,
58+
runDetails.mainFileTypingContent,
59+
),
60+
);
61+
62+
for (var i = 0; i < 3; i++) {
63+
// Send 'getAssists'...
64+
var getAssistsStopwatch = Stopwatch()..start();
65+
var getAssistsFuture = (await send(
66+
LegacyMessages.getAssists(
67+
largestIdSeen + 1,
68+
runDetails.orderedFileCopies.last.uri,
69+
runDetails.typingAtOffset,
70+
),
71+
))!.completer.future.then((_) => getAssistsStopwatch.stop());
72+
73+
// ...and ask for completion.
74+
var firstCompletionStopwatch = Stopwatch()..start();
75+
var completionResult =
76+
await (await send(
77+
LegacyMessages.getSuggestions2(
78+
largestIdSeen + 1,
79+
runDetails.mainFile.uri,
80+
runDetails.typingAtOffset,
81+
),
82+
))!.completer.future;
83+
firstCompletionStopwatch.stop();
84+
List<dynamic> completionItems =
85+
completionResult['result']['suggestions'] as List;
86+
durationInfo.add(
87+
DurationInfo(
88+
'Completion call ${i + 1}',
89+
firstCompletionStopwatch.elapsed,
90+
),
91+
);
92+
if (verbosity >= 0) {
93+
print(
94+
'Got ${completionItems.length} completion items '
95+
'in ${firstCompletionStopwatch.elapsed}',
96+
);
97+
}
98+
99+
// This should be complete already.
100+
await getAssistsFuture;
101+
102+
// This is not really the interesting part (it's slow because of the plugin
103+
// that times out), but let's include it anyway.
104+
durationInfo.add(
105+
DurationInfo('getAssists call ${i + 1}', getAssistsStopwatch.elapsed),
106+
);
107+
}
108+
}
109+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 '../lsp_messages.dart';
7+
import 'utils.dart';
8+
9+
/// We've observed that sometimes the plugin that users has installed times out
10+
/// (takes > 500 ms to answer). This benchmark simulates that and shows the
11+
/// worse handling of this introduced in Dart 3.7.
12+
Future<void> main(List<String> args) async {
13+
await runHelper(
14+
args,
15+
LspWithPluginThatTimesOutBencmark.new,
16+
runAsLsp: true,
17+
includePlugin: true,
18+
// The number of files isn't important here.
19+
numberOfFileOptions: [10],
20+
);
21+
}
22+
23+
class LspWithPluginThatTimesOutBencmark extends DartLanguageServerBenchmark {
24+
@override
25+
final Uri rootUri;
26+
@override
27+
final Uri cacheFolder;
28+
29+
final RunDetails runDetails;
30+
31+
LspWithPluginThatTimesOutBencmark(
32+
super.args,
33+
this.rootUri,
34+
this.cacheFolder,
35+
this.runDetails,
36+
) : super(useLspProtocol: true);
37+
38+
@override
39+
LaunchFrom get launchFrom => LaunchFrom.Dart;
40+
41+
@override
42+
Future<void> afterInitialization() async {
43+
await send(
44+
LspMessages.open(runDetails.mainFile.uri, 1, runDetails.mainFile.content),
45+
);
46+
47+
// Does this change anything?
48+
await Future.delayed(const Duration(milliseconds: 100));
49+
50+
// The user starts typing - types 'ge' in the empty line in main...
51+
await send(
52+
LspMessages.didChange(
53+
runDetails.mainFile.uri,
54+
version: 2,
55+
insertAtLine: 3 /* line 0-indexed; at blank line inside main */,
56+
insertAtCharacter: 2,
57+
insert: 'ge',
58+
),
59+
);
60+
61+
for (var i = 0; i < 3; i++) {
62+
// Send 'textDocument/codeAction'...
63+
var codeActionStopwatch = Stopwatch()..start();
64+
var codeActionFuture = (await send(
65+
LspMessages.codeAction(
66+
largestIdSeen + 1,
67+
runDetails.mainFile.uri,
68+
line: 3,
69+
character: 4 /* after the 'ge' just typed */,
70+
),
71+
))!.completer.future.then((_) => codeActionStopwatch.stop());
72+
73+
// ...and ask for completion.
74+
var firstCompletionStopwatch = Stopwatch()..start();
75+
var completionResult =
76+
await (await send(
77+
LspMessages.completion(
78+
runDetails.mainFile.uri,
79+
largestIdSeen + 1,
80+
line: 3,
81+
character: 4 /* after the 'ge' just typed */,
82+
),
83+
))!.completer.future;
84+
firstCompletionStopwatch.stop();
85+
List<dynamic> completionItems =
86+
completionResult['result']['items'] as List;
87+
durationInfo.add(
88+
DurationInfo(
89+
'Completion call ${i + 1}',
90+
firstCompletionStopwatch.elapsed,
91+
),
92+
);
93+
if (verbosity >= 0) {
94+
print(
95+
'Got ${completionItems.length} completion items '
96+
'in ${firstCompletionStopwatch.elapsed}',
97+
);
98+
}
99+
100+
// This should be complete already.
101+
await codeActionFuture;
102+
103+
// This is not really the interesting part (it's slow because of the plugin
104+
// that times out), but let's include it anyway.
105+
durationInfo.add(
106+
DurationInfo('codeAction call ${i + 1}', codeActionStopwatch.elapsed),
107+
);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)