Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"flutter.setSubscriptions" call never forgets a file (aka will ask about an ever growing number of files) #7980

Open
jensjoha opened this issue Mar 24, 2025 · 9 comments
Assignees
Labels
Milestone

Comments

@jensjoha
Copy link

In IntelliJ, opening a project with flutter in the environment in pubspec.yaml, e.g. something like

name: issue_whatnot
publish_to: "none"

environment:
  flutter: ">=3.29.0 <4.0.0"
  sdk: ">=3.7.0 <4.0.0"

opening a file sends a flutter.setSubscriptions request looking something like this:

{
  "id": "42",
  "method": "flutter.setSubscriptions",
  "params": {
    "subscriptions": {
      "OUTLINE": [
        "file:///path/to/file_1.dart"
      ]
    }
  }
}

Say we close the file and open another, it now sends something like this:

{
  "id": "42",
  "method": "flutter.setSubscriptions",
  "params": {
    "subscriptions": {
      "OUTLINE": [
        "file:///path/to/file_1.dart",
        "file:///path/to/file_2.dart"
      ]
    }
  }
}

closing that and opening a third, closing that and opening a forth etc a bunch of times and you'll end up with something like

{
  "id": "42",
  "method": "flutter.setSubscriptions",
  "params": {
    "subscriptions": {
      "OUTLINE": [
        "file:///path/to/file_1.dart",
        "file:///path/to/file_2.dart",
        ...
        "file:///path/to/file_n.dart",
      ]
    }
  }
}

at which point the analyzer will start to become slow:

5 ms (+ 22045 ms)	completion.getSuggestions2
23121 ms (+ 100 ms)	edit.getAssists

Notice that if I open a file I've already opened once it doesn't send the request, I believe because of this:

private void addSubscription(@NotNull final String service, @NotNull final String filePath) {
if(!isServerConnected()) {
return;
}
final List<String> files = subscriptions.computeIfAbsent(service, k -> new ArrayList<>());
final String filePathOrUri = getAnalysisService().getLocalFileUri(filePath);
if (!files.contains(filePathOrUri)) {
files.add(filePathOrUri);
sendSubscriptions();
}
}
.

(i.e. sendSubscriptions is only called i it actually adds the path)

I'm guessing something goes wrong in these lines:

final List<String> obsoletePaths = new ArrayList<>();
synchronized (outlineListeners) {
for (final String path : outlineListeners.keySet()) {
if (!newPaths.contains(path)) {
obsoletePaths.add(path);
}
}
for (final String path : obsoletePaths) {
final String filePathOrUri = getAnalysisServer().getAnalysisService().getLocalFileUri(path);
final FlutterOutlineListener listener = outlineListeners.remove(filePathOrUri);
if (listener != null) {
getAnalysisServer().removeOutlineListener(FileUtil.toSystemDependentName(path), listener);
}
}
// Register new outline listeners.
for (final String path : newPaths) {
final String filePathOrUri = getAnalysisServer().getAnalysisService().getLocalFileUri(path);
if (outlineListeners.containsKey(filePathOrUri)) {
continue;
}
final FlutterOutlineListener listener = new OutlineListener(filePathOrUri);
outlineListeners.put(filePathOrUri, listener);
getAnalysisServer().addOutlineListener(FileUtil.toSystemDependentName(path), listener);
}
}

Without having looked into it it seems suspect that it takes a key from outlineListeners (final String path : outlineListeners.keySet()) and then does something to it before trying to remove it again (final String filePathOrUri = getAnalysisServer().getAnalysisService().getLocalFileUri(path);, final FlutterOutlineListener listener = outlineListeners.remove(filePathOrUri);), but I'm guessing.

@jensjoha
Copy link
Author

/cc @jwren

copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Mar 25, 2025
…ening new files

This benchmark simulates the current bug at
flutter/flutter-intellij#7980

Comparing this benchmark across dart versions also reveals something
interesting, here run on the "ImportChain" type with 100 files:

Comparing 3.5.4 with 3.6.2
```
Initial analysis: -2.5724% +/- 1.5022% (-0.16 +/- 0.10) (6.35 -> 6.19)
Completion after open of new file: -1.4714% +/- 1.0334% (-0.04 +/- 0.03) (2.92 -> 2.87)
peak virtual memory size: 8.6445% +/- 2.4585% (220.40 +/- 62.68) (2549.60 -> 2770.00)
total program size (virtual): 9.0756% +/- 2.4049% (226.20 +/- 59.94) (2492.40 -> 2718.60)
peak resident set size ("high water mark"): -9.7940% +/- 1.6649% (-58.00 +/- 9.86) (592.20 -> 534.20)
size of memory portions (rss): -8.7961% +/- 3.4971% (-47.20 +/- 18.77) (536.60 -> 489.40)
```

Comparing 3.6.2 with 3.7.2
```
Initial analysis: -8.7696% +/- 2.2759% (-0.54 +/- 0.14) (6.19 -> 5.64)
Completion without opening files: 16.5289% +/- 8.3591% (0.07 +/- 0.03) (0.41 -> 0.48)
Completion after open of new file: 45.0913% +/- 2.8023% (1.30 +/- 0.08) (2.87 -> 4.17)
getAssists call: 21.4736% +/- 2.3049% (0.61 +/- 0.07) (2.86 -> 3.48)
peak virtual memory size: -5.9134% +/- 3.8164% (-163.80 +/- 105.72) (2770.00 -> 2606.20)
total program size (virtual): -6.5548% +/- 4.0754% (-178.20 +/- 110.79) (2718.60 -> 2540.40)
peak resident set size ("high water mark"): -16.3984% +/- 0.5460% (-87.60 +/- 2.92) (534.20 -> 446.60)
size of memory portions (rss): -18.0629% +/- 3.2699% (-88.40 +/- 16.00) (489.40 -> 401.00)
```

Where, between 3.6.2 and 3.7.2 these stand out:
```
Completion without opening files: 16.5289% +/- 8.3591% (0.07 +/- 0.03) (0.41 -> 0.48)
Completion after open of new file: 45.0913% +/- 2.8023% (1.30 +/- 0.08) (2.87 -> 4.17)
getAssists call: 21.4736% +/- 2.3049% (0.61 +/- 0.07) (2.86 -> 3.48)
```

these surely weren't great before, but are much worse now.

Change-Id: I6f94f941cda86b1aa9ee7e7a9b1912df85c7acb2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/417820
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
@bwilkerson
Copy link

@jwren Is it true that IntelliJ should never need to get flutter outline information for more than one file at a time?

@jwren jwren added this to the M85 milestone Mar 27, 2025
@jwren
Copy link
Member

jwren commented Mar 27, 2025

I am not familiar enough with the Flutter setSubscriptions protocol (https://htmlpreview.github.io/?https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/doc/api.html#request_flutter.setSubscriptions) to be able to come in and determine if the right things are being done here from the IJ side without lots of manual testing and digging into the protocol. It looks like the feature was largely added by Konstantin and Jacob. Thanks for starting to look into this issue to get better performance.

copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 1, 2025
…ptions" are not; non-interactive ones have less priority

The legacy_many_files_in_flutter_set_subscriptions benchmark shows how
"flutter.setSubscriptions" calls can make the analyzer slower to
respond.

What happens is this:

* The user opens a new file in the IDE.
* The IDE sends the `flutter.setSubscriptions` request which equates to
  a call to `getResolvedUnit` for each file in the request. If this is,
  say, 300 files it's 300 calls to `getResolvedUnit`.
* The IDE sends a `edit.getAssists` request for the newly opened file.
  This request starts processing, reaches `getResolvedLibrary(file)`
  which calls `getUnitElement` ultimately adding the path to
  `_unitElementRequestedFiles` which in `performWork` is done _after_
  `_requestedFiles`, meaning it has to do all the flutter requested
  files first.
* The user might then request completion for instance, but because the
  analyzer only processes one request at a time it has to wait for the
  `edit.getAssists` request to finish first, which had to wait for the
  files from the `flutter.setSubscriptions` request to process.

All in all it's a lot of waiting for the user.

This CL adds a `interactive` option to the `getResolvedUnit` call. It
defaults to true in which case files are still added to
`_requestedFiles` and processed the same. If it's false it will instead
be added to a newly introduced list instead and processed at a lower
priority. Subscription requests are changed to pass `false` to
`interactive`, avoiding the scenario above.

Comparing before this CL with this CL on the
"legacy_many_files_in_flutter_set_subscriptions" benchmark with 100
files / CodeType.ImportChain these are the statistics on the changes
based on 5 runs each:

```
Completion after open of new file: -81.6652% +/- 7.7564% (-3.70 +/- 0.35) (4.53 -> 0.83)
getAssists call: -96.6315% +/- 0.9307% (-3.61 +/- 0.03) (3.74 -> 0.13)
peak virtual memory size: -5.6786% +/- 3.2964% (-139.00 +/- 80.69) (2447.80 -> 2308.80)
total program size (virtual): -4.6387% +/- 3.8146% (-110.80 +/- 91.11) (2388.60 -> 2277.80)
```

Even when flutter/flutter-intellij#7980 is
hopefully fixed I think it is a fair change to de-prioritize a
non-interactive request.

Change-Id: Icba2faebf12f9913cf24db7cb90fdc6f4c74164e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/418020
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
@pq pq self-assigned this Apr 3, 2025
@pq
Copy link
Contributor

pq commented Apr 3, 2025

Thanks for this great report @jensjoha!

Taking a look at this now.

@alexander-doroshko
Copy link
Contributor

The the fileOutlineListeners map grows indefinitely because

If all code related to the "flutter.setSubscriptions" call is not needed anymore, this map may be completely removed along with a bunch of other obsolete code

@pq
Copy link
Contributor

pq commented Apr 3, 2025

If all code related to the "flutter.setSubscriptions" call is not needed anymore, this map may be completely removed along with a bunch of other obsolete code

Right! I'm trying to establish exactly this. It looks like CommonTestConfigUtils uses this service (see, for example, getTestsFromOutline) but I'm not sure if this is being used (or works as intended). Will dig a little more.

Thanks!

@pq
Copy link
Contributor

pq commented Apr 3, 2025

On the topic of managing the file map, it seems like the issue is introduced (or at least manifest) here:

for (final String path : obsoletePaths) {
final String filePathOrUri = getAnalysisServer().getAnalysisService().getLocalFileUri(path);
final FlutterOutlineListener listener = outlineListeners.remove(filePathOrUri);

The rub is that the path in obsoletePaths is already a file path (for example file:///Users/pq/StudioProjects/fluttery/lib/main.dart). When such a path is passed to getLocalFileUri it gets turned into something like file://file:///Users/pq/StudioProjects/fluttery/lib/main.dart, which is incorrect and so the listener is never removed.

@alexander-doroshko: it seems like getLocalFileUri could be improved in the Dart plugin DartAnalysisServerService to do the right thing in case a valid file path is passed into it?

In the meantime, I think using the obsolete path as-is addresses the problem:

for (final String path : obsoletePaths) {
  final FlutterOutlineListener listener = outlineListeners.remove(path);
  ...

@pq
Copy link
Contributor

pq commented Apr 3, 2025

As for the use of the flutter outline service in general, even though we are no longer displaying a flutter outline view, it does seem like we're using the the service to figure out where to contribute test line markers.

See:

final TestType testCall = testConfigUtils.asTestCall(dartCallExpression);

which calls here:

public TestType asTestCall(@NotNull PsiElement element) {

I've confirmed that this code is alive and does contribute to markers showing up

Image

but I'm not sure it's entirely working as intended. Either way, in the absences of a flutter outline view, it's likely this isn't the best way to calculate test line markers regardless but I'm missing a lot of context and need to do some more digging but maybe we should create another issue for that and just focus on getting the subscribed file list to stop growing infinitely.

@alexander-doroshko
Copy link
Contributor

The rub is that the path in obsoletePaths is already a file path (for example file:///Users/pq/StudioProjects/fluttery/lib/main.dart)

You mean 'already a URI' :)

@alexander-doroshko: it seems like getLocalFileUri could be improved in the Dart plugin DartAnalysisServerService to do the right thing in case a valid file path is passed into it?

You mean 'to do the right thing in case a valid file URI is passed into it' :)

I'd say passing URI to DartAnalysisServerService.getLocalFileUri() is an incorrect API usage. I think getLocalFileUri() shouldn't try to fix caller mistake. I'd rather make it throw IllegalArgumentException. If it had done that, we'd have caught the problem a year ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants