Skip to content

Commit b79056a

Browse files
jensjohaCommit Queue
authored andcommitted
[analyzer] Don't calculate add-late assists for other files
The "add late" assist was broadened in https://dart-review.googlesource.com/c/sdk/+/176900 with a link to #44440 where the example is adding `late` to a field in the same class the user is currently editing the constructor for. That makes sense. The code could also add the `late` keyword to other files which is - I think - bad for at least two reasons: 1) It's confusing being given the option to "add late" to something, then when trying it nothing seemingly happen, except something does happen: another - possibly not open - file has changed. 2) It requests the resolved unit for another - possibly not open - file, which is slow. This means that, at least in VSCode, just moving the cursor over something can send requests that takes a long time. In practise I've experienced up to ~1.5 seconds, and in benchmarks I can make this arbitrarily large by increasing the size of the file. Shown below, having 16,000 small classes takes around 4 seconds (vs less than 10 ms with this CL). This CL restricts this to the same file, adds a few tests and updates a few existing tests. Note that the existing tests that verified the behavior of adding `late` to other files was added in https://dart-review.googlesource.com/c/sdk/+/180087 with a link to #44534 where the issue discusses a bug where it is applied in the wrong file and says that it should either be applied to the right file or be disallowed. The option of allowing it was picked in that instance, but now at least there's data to show that it comes at a cost. For an ad-hoc test where I programmatically asks for assists at every position in `pkg/front_end/lib/src/kernel/body_builder.dart` I go from 1875 positions that takes >= 100 ms to answer to 0 such positions. For the added benchmark I get this data: Before this CL: ``` size 1000: Initial analysis: 0.188291 Action call on 4:11 : 0.308514 Action call on 4:12 : 0.319540 Action call on 4:13 : 0.274678 peak virtual memory size: 2363 MB total program size (virtual): 2292 MB peak resident set size ("high water mark"): 258 MB size of memory portions (rss): 244 MB size 2000: Initial analysis: 0.387643 Action call on 4:11 : 0.649877 Action call on 4:12 : 0.550778 Action call on 4:13 : 0.474030 peak virtual memory size: 2325 MB total program size (virtual): 2325 MB peak resident set size ("high water mark"): 279 MB size of memory portions (rss): 277 MB size 4000: Initial analysis: 0.753913 Action call on 4:11 : 1.086648 Action call on 4:12 : 1.015921 Action call on 4:13 : 0.915511 peak virtual memory size: 2335 MB total program size (virtual): 2304 MB peak resident set size ("high water mark"): 363 MB size of memory portions (rss): 334 MB size 8000: Initial analysis: 1.235531 Action call on 4:11 : 1.880335 Action call on 4:12 : 1.824658 Action call on 4:13 : 1.771081 peak virtual memory size: 2414 MB total program size (virtual): 2386 MB peak resident set size ("high water mark"): 436 MB size of memory portions (rss): 411 MB size 16000: Initial analysis: 2.618666 Action call on 4:11 : 3.991542 Action call on 4:12 : 3.775863 Action call on 4:13 : 4.094692 peak virtual memory size: 2576 MB total program size (virtual): 2516 MB peak resident set size ("high water mark"): 667 MB size of memory portions (rss): 611 MB ``` With this CL: ``` size 1000: Initial analysis: 0.202665 Action call on 4:11 : 0.005730 Action call on 4:12 : 0.003086 Action call on 4:13 : 0.002743 peak virtual memory size: 2174 MB total program size (virtual): 2170 MB peak resident set size ("high water mark"): 256 MB size of memory portions (rss): 247 MB size 2000: Initial analysis: 0.433420 Action call on 4:11 : 0.005353 Action call on 4:12 : 0.002116 Action call on 4:13 : 0.002156 peak virtual memory size: 2226 MB total program size (virtual): 2226 MB peak resident set size ("high water mark"): 290 MB size of memory portions (rss): 240 MB size 4000: Initial analysis: 0.674376 Action call on 4:11 : 0.004219 Action call on 4:12 : 0.002138 Action call on 4:13 : 0.001625 peak virtual memory size: 2392 MB total program size (virtual): 2328 MB peak resident set size ("high water mark"): 306 MB size of memory portions (rss): 286 MB size 8000: Initial analysis: 1.244688 Action call on 4:11 : 0.005225 Action call on 4:12 : 0.002104 Action call on 4:13 : 0.002729 peak virtual memory size: 2349 MB total program size (virtual): 2338 MB peak resident set size ("high water mark"): 385 MB size of memory portions (rss): 366 MB size 16000: Initial analysis: 2.776680 Action call on 4:11 : 0.008848 Action call on 4:12 : 0.002854 Action call on 4:13 : 0.002327 peak virtual memory size: 2423 MB total program size (virtual): 2405 MB peak resident set size ("high water mark"): 505 MB size of memory portions (rss): 489 MB ``` It's interesting how the action calls for sizes >= 4000 was slower than the initial analysis, but I haven't looked into it. Change-Id: Icefe02073cdf1ad442a37de2030fd7e53f5013b9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/436280 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Jens Johansen <[email protected]>
1 parent 6c7e3a6 commit b79056a

File tree

5 files changed

+194
-13
lines changed

5 files changed

+194
-13
lines changed

pkg/analysis_server/lib/src/services/correction/dart/add_late.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ class AddLate extends ResolvedCorrectionProducer {
7171
if (variableElement != null &&
7272
!variableElement.isSynthetic &&
7373
!variableElement.isLate &&
74-
variableElement.setter2 == null) {
74+
variableElement.setter2 == null &&
75+
// It is currently too expensive to do a `getFragmentDeclaration`
76+
// call if we don't already have the resolved library ready.
77+
// If it becomes desirable to allow such edits we'll likely need
78+
// to do something else to not regress performance.
79+
variableElement.firstFragment.libraryFragment.source.fullName ==
80+
file) {
7581
var variableFragment = variableElement.firstFragment;
7682
var declarationResult = await sessionHelper.getFragmentDeclaration(
7783
variableFragment,

pkg/analysis_server/test/src/services/correction/assist/add_late_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,48 @@ class C {
5555
''');
5656
}
5757

58+
Future<void> test_field_finalType_in_other_file() async {
59+
late var test2FilePath = '$testPackageLibPath/test2.dart';
60+
var test2File = getFile(test2FilePath);
61+
newFile(test2File.path, '''
62+
class C {
63+
final String s;
64+
C(this.s);
65+
}
66+
''');
67+
await resolveTestCode('''
68+
import 'test2.dart';
69+
70+
void foo() {
71+
C c = C('42');
72+
c.s^;
73+
}
74+
''');
75+
76+
// Don't give assists for another file.
77+
await assertNoAssist();
78+
}
79+
80+
Future<void> test_field_finalType_when_in_constructor() async {
81+
verifyNoTestUnitErrors = false;
82+
await resolveTestCode('''
83+
class C {
84+
final String s;
85+
C() {
86+
s^ = '';
87+
}
88+
}
89+
''');
90+
await assertHasAssist('''
91+
class C {
92+
late final String s;
93+
C() {
94+
s = '';
95+
}
96+
}
97+
''');
98+
}
99+
58100
Future<void> test_field_type() async {
59101
await resolveTestCode('''
60102
class C {

pkg/analysis_server/test/src/services/correction/assist/assist_processor.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ abstract class AssistProcessorTest extends AbstractSingleUnitTest {
8686
var fileEdits = _change.edits;
8787
if (additionallyChangedFiles == null) {
8888
expect(fileEdits, hasLength(1));
89+
expect(_change.edits[0].file, testFilePath);
8990
_resultCode = SourceEdit.applySequence(testCode, _change.edits[0].edits);
9091
expect(_resultCode, expected);
9192
} else {

pkg/analysis_server/test/src/services/correction/fix/add_late_test.dart

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ void f(C c) {
3333
c.s = '';
3434
}
3535
''');
36-
await assertHasFix('''
37-
class C {
38-
late final String s;
39-
}
40-
''', target: '$testPackageLibPath/a.dart');
36+
await assertNoFix();
4137
}
4238

4339
Future<void> test_changeInPart() async {
@@ -55,13 +51,7 @@ void f(C c) {
5551
c.s = '';
5652
}
5753
''');
58-
await assertHasFix('''
59-
part 'test.dart';
60-
61-
class C {
62-
late final String s;
63-
}
64-
''', target: '$testPackageLibPath/a.dart');
54+
await assertNoFix();
6555
}
6656

6757
Future<void> test_final_implicitThis() async {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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:io';
6+
7+
import '../language_server_benchmark.dart';
8+
import '../lsp_messages.dart';
9+
import '../run_utils.dart';
10+
11+
/// Small file imports big file. Asking for assists in possitions in the small
12+
/// file should not be expensive (i.e. should not require loading more of the
13+
/// big file).
14+
/// Previously the "add late" assist would load the big file and make this slow.
15+
Future<void> main(List<String> args) async {
16+
await runHelper(
17+
args,
18+
LspAssistLate.new,
19+
createData,
20+
sizeOptions: [1000, 2000, 4000, 8000, 16000],
21+
extraIterations: (_) => [null],
22+
runAsLsp: true,
23+
);
24+
}
25+
26+
RunDetails createData(
27+
Uri packageDirUri,
28+
Uri outerDirForAdditionalData,
29+
int size,
30+
dynamic unused,
31+
List<String> args, {
32+
// unused
33+
required dynamic extraInformation,
34+
}) {
35+
Uri libDirUri = packageDirUri.resolve('lib/');
36+
Directory.fromUri(libDirUri).createSync();
37+
38+
{
39+
var libUri = libDirUri.resolve('lib.dart');
40+
var libContent = StringBuffer();
41+
for (int i = 0; i < size; i++) {
42+
var className = 'Class$i';
43+
libContent.write('''
44+
class $className {
45+
final int foo;
46+
$className(this.foo) {
47+
print("Hello from class $className");
48+
print("$className.foo = \$foo");
49+
}
50+
}
51+
''');
52+
}
53+
var libContentString = libContent.toString();
54+
File.fromUri(libUri).writeAsStringSync(libContentString);
55+
}
56+
57+
var mainFileUri = libDirUri.resolve('main.dart');
58+
var mainFileContentString = '''
59+
import 'lib.dart';
60+
61+
void main() {
62+
var c1 = Class1(42);
63+
print(c1.foo);
64+
}
65+
''';
66+
File.fromUri(mainFileUri).writeAsStringSync(mainFileContentString);
67+
68+
return RunDetails(
69+
mainFile: FileContentPair(mainFileUri, mainFileContentString),
70+
);
71+
}
72+
73+
class FileContentPair {
74+
final Uri uri;
75+
final String content;
76+
77+
FileContentPair(this.uri, this.content);
78+
}
79+
80+
class LspAssistLate extends DartLanguageServerBenchmark {
81+
@override
82+
final Uri rootUri;
83+
@override
84+
final Uri cacheFolder;
85+
86+
final RunDetails runDetails;
87+
88+
LspAssistLate(super.args, this.rootUri, this.cacheFolder, this.runDetails)
89+
: super(useLspProtocol: true);
90+
91+
@override
92+
LaunchFrom get launchFrom => LaunchFrom.Dart;
93+
94+
@override
95+
Future<void> afterInitialization() async {
96+
var lines = runDetails.mainFile.content.split('\n');
97+
await send(
98+
LspMessages.open(runDetails.mainFile.uri, 1, runDetails.mainFile.content),
99+
);
100+
101+
Future<Duration> timeAssist(int lineNumber, int column) async {
102+
var codeActionStopwatch = Stopwatch()..start();
103+
var codeActionFuture = (await send(
104+
LspMessages.codeAction(
105+
largestIdSeen + 1,
106+
runDetails.mainFile.uri,
107+
line: lineNumber,
108+
character: column,
109+
),
110+
))!.completer.future.then((result) {
111+
codeActionStopwatch.stop();
112+
return result;
113+
});
114+
await codeActionFuture;
115+
return codeActionStopwatch.elapsed;
116+
}
117+
118+
const lineNumber = 4;
119+
String line = lines[lineNumber];
120+
if (line != ' print(c1.foo);') {
121+
throw 'Unexpected change in benchmark.';
122+
}
123+
124+
// Warmup.
125+
for (int i = 0; i < 10; i++) {
126+
await timeAssist(lineNumber, 0);
127+
}
128+
129+
for (int column = 11; column < 14; column++) {
130+
var duration = await timeAssist(lineNumber, column);
131+
durationInfo.add(
132+
DurationInfo('Action call on $lineNumber:$column ', duration),
133+
);
134+
}
135+
}
136+
}
137+
138+
class RunDetails {
139+
final FileContentPair mainFile;
140+
141+
RunDetails({required this.mainFile});
142+
}

0 commit comments

Comments
 (0)