Skip to content

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

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

Closed
jensjoha opened this issue Mar 24, 2025 · 11 comments · Fixed by #8057
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.

@pq
Copy link
Contributor

pq commented Apr 7, 2025

You mean 'already a URI' :)

Yes, right! The trouble is, when we're passing strings around, it's hard to tell. 😄

That said, if getLocalFileUri threw an exception we'd be better off for sure.

Anyway, I'll put together a fix for this and if things get hardened up-stream, all the better.

Thanks!

@alexander-doroshko
Copy link
Contributor

we're passing strings around, it's hard to tell. 😄

True.
Macros are gone but code complexity remains :)
Just FYI, here’s an approach I followed while migrating from paths to URIs in the Dart plugin. This could help with reading the Dart plugin source code and updating the Flutter plugin.

Note that this text was written a year ago. Given that the Flutter plugin has been already updated (hopefully, according to these recommendations), and that macros have gone since then, this text might be partially obsolete.

  1. Sending data to the Analysis Server
  • try not to work with paths, work with VirtualFiles
  • in some cases working with VirtualFile is impossible, you need to work with paths, but you must be sure that those are local file paths. Because such thing as ‘macro-generated’ file path doesn’t make sense, those files have only URIs
  • convert VirtualFile to path or uri as late as possible, right before sending to the Analysis Server, don’t use this string (path or uri) for anything else
  • com.jetbrains.lang.dart.analyzer.DartAnalysisServerService#getFileUri does the trick
    • for SDK versions 3.3 and older it returns plain file path with system-dependent slashes
    • for SDK versions 3.4 and newer it returns URI, as expected by the server
  • when you don’t have a VirtualFile but only have a file path and need to send it to the AnalysisServer
    • you must be sure that this is a local file path. Such thing as ‘macro-generated’ file path doesn’t make sense
    • use com.jetbrains.lang.dart.analyzer.DartAnalysisServerService#getLocalFileUri
    • for SDK versions 3.3 and older it returns plain file path with system-dependent slashes
    • for SDK versions 3.4 and newer it returns URI, as expected by the server
    • again, don’t use the returned string for anything else, only for sending to the server
  • example: DartAnalysisServerService.search_findElementReferences and more in this class
  • The methods getFileUri and getLocalFileUri are public for the Flutter plugin. In the Dart plugin they are used only in the DartAnalysisServerService class and are not expected to be used anywhere else
  1. Handling data received from the AnalysisServer
  • use filePathOrUri name for parameters/variables that store strings received from the Analysis Server
  • after receiving, immediately call fileInfo=com.jetbrains.lang.dart.analyzer.DartFileInfoKt#getDartFileInfo(project, filePathOrUri)
  • don’t use the received filePathOrUri string anymore, use DartFileInfo object
  • most likely you’ll only need com.jetbrains.lang.dart.analyzer.DartFileInfo#findFile
  • in rare cases you may need to check if it is an instance of DartLocalFileInfo or DartNotLocalFileInfo and use DartLocalFileInfo.filePath or DartNotLocalFileInfo.fileUri. Think twice whether you really need it
  1. Looking for code in the plugin that needs to be updated
  • all getFile() methods of all classes in org.dartlang.analysis.server.protocol now return filePathOrUri. For example, Position.getFile(). I haven’t found such usages in the Flutter plugin but it would be good to double-check
  • all calls of all methods of the com.google.dart.server.internal.remote.RemoteAnalysisServerImpl class
  • all implementations of Analysis Server-related listeners. Like com.google.dart.server.AnalysisServerListener. Its methods now get file path or uri. For example, com.google.dart.server.AnalysisServerListener#computedErrors(String file, List<AnalysisError> errors). Its parameter is called file, but it’s not always a file path. Call the parameter filePathOrUri in the implementation
  • check all toSystemDependentName() and toSystemIndependentName(). The rule of thumb is:
    • call toSystemDependentName() as late as possible to show the text in UI. for example, textField.setText(toSystemDependentName(file.getPath))
    • call toSystemIndependentName() as early as possible right after getting the text from a UI component
    • if you need to work with paths, work only with system independent paths everywhere else
  • check all LocalFileSystem.findXxx() methods. Use DartFileInfo.findFile() instead where needed.

@pq pq modified the milestones: M85, M86, M85.1 Apr 8, 2025
jwren pushed a commit that referenced this issue Apr 14, 2025
Fixes an issue where we never unsubscribe outline listeners.

The rub is that `getLocalFileUri` is being called on a path that is
already a URI and is producing a bogus path which has no associated
listener registered (because it’s bogus).

In general the issue here is a symptom of URIs being encoded in strings
that are never validated.

The quick fix is to not “uri-ify” an already valid UR string. Longer
term we should consider a deeper audit of places we’re passing URIs
around. It’s possible there are other related issues. If/when we do
this, Alex shared some [interesting thoughts from his work on the Dart
plugin](#7980 (comment))
that’d be worth synthesizing.

Fixes: #7980

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants