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

"getCompletion" from TypeScript LSP scans the entire project for each file. #2329

Open
MrWaip opened this issue Apr 3, 2024 · 25 comments
Open
Labels
perf Performance

Comments

@MrWaip
Copy link

MrWaip commented Apr 3, 2024

Describe the bug

Issue: A request for completions from the editor in my working project takes 10 seconds. I decided to investigate why. Every subsequent request for completions from the same Svelte document is instantaneous. However, as soon as I open another Svelte component, I have to wait another 10 seconds.

I deployed language tools locally and configured them for my working project. Then I started measuring where the bottleneck was. That's how I ended up in CompletionProvider (packages/language-server/src/plugins/typescript/features/CompletionProvider.ts). And the slow call turned out to be the one where we access TypeScript (const response = lang.getCompletionsAtPosition()).

I delved even deeper and realized that the slowest request for completions in the TypeScript LSP is function getGlobalCompletions().

This function attempts to suggest completions for imports, and this takes the entire 10 seconds (collectAutoImports -> exportInfo.search()). TypeScript caches this result, but if I open another file, it starts over. This is very strange. Are you creating a TypeScript LSP for every Svelte file? In my project, the TypeScript LSP scans an array of 23,580 elements for every completion request. And I suppose it also transforms Svelte files internally to understand what can be imported from there. This is a huge amount of cache for every file.

image

Reproduction

I don't know. Just ask for completions

Expected behaviour

I would like the completion request to be executed faster and not to be repeated when opening another Svelte document.

System Info

  • OS: MAC
  • IDE: VS Code

Which package is the issue about?

No response

Additional Information, eg. Screenshots

1e907842-3376-4479-862f-84b6deb1fbbf.mp4
@MrWaip MrWaip added the bug Something isn't working label Apr 3, 2024
@dummdidumm dummdidumm added the perf Performance label Apr 3, 2024
@jasonlyu123
Copy link
Member

jasonlyu123 commented Apr 3, 2024

This is most likely because of the icon library you're using. Some icon libraries bloat the project dependencies with a ton of files that take a long time to process. This icon library seems to be a private package so we can't investigate what exactly the bottleneck is. I guess it might be something similar to #2244 (comment). It might not have the big export map issue. However, the parser error stage issue might still apply.

This is very strange. Are you creating a TypeScript LSP for every Svelte file

No. We don't create a TypeScript language service for every file. But TypesScript will need to check if the cache is still usable in the new file since some relative path might change. Some auto import in the cache might be inaccessible in the new file. But it is still strange for it to take this long. We still need a reproduction to check why the cache in missing or why the cache got invalidated.

@MrWaip
Copy link
Author

MrWaip commented Apr 4, 2024

This is our private UI kit. It contains around 2500 icons. I even had to write a plugin for esbuild and vite to optimize imports from the index.js file, which re-exports these icons.

I can try to create a simulation of our project where we could reproduce this issue.

Is it possible not to preprocess the Svelte component for import completions? Maybe introduce some flag that would skip this process, which could speed up this process. Then we would lose component typings, right?

Or maybe for import completions, it's enough to assume that the default export is always exported from there, and that's it, and then preprocess the component when using it to get typings.

@MrWaip
Copy link
Author

MrWaip commented Apr 4, 2024

I also decided to check if the cache is lost when I try to import Svelte components in different TypeScript files, and it doesn't happen. The cache remains, and getCompletion works quickly.

@jasonlyu123
Copy link
Member

We can't tell if it's a problem with our implementation unless there's a reproduction. I tried multiple icon libraries but didn't reproduce the problem with switching files. The export info map is cleared when switching files but there are still other caches so it's not completely restart.

In the meanwhile, You can probably try to exclude the icon library from auto-import with the javascript.preferences.autoImportFileExcludePatterns or typescript.preferences.autoImportFileExcludePatterns config.

@MrWaip
Copy link
Author

MrWaip commented Apr 4, 2024

OK, I'll try reproduce

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

@jasonlyu123 Hello. Here is the repository where you can verify that the cache is not reused between files.

https://github.com/MrWaip/svelte-lsp-slow-complitions

You can open the file src/routes/+page.svelte, request auto import completions, then open src/routes/another/+page.svelte and do the same, and make sure it takes a lot of time again.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

I continue exploring LSP.

Your suggestion to exclude icons in typescript.preferences.autoImportFileExcludePatterns helped in one project. The cache stopped disappearing between files, and the response without cache comes in 3 seconds instead of 10.

The project I'm talking about was small.

Project files: 582
Svelte files: 98
From node_modules: 0
Total: 582

I decided to add this setting to a larger project. It didn't help for some reason. I disabled all extensions. Turned off almost all features of Svelte LSP. Enabled debug mode and trace in the settings of the Svelte extension.

Here's the trace:


[Trace - 16:14:59] Sending request 'textDocument/completion - (14)'.
Params: {
    "textDocument": {
        "uri": "file:///Users/user/code/project/src/pages/Bonus/Bonus.desktop.svelte"
    },
    "position": {
        "line": 12,
        "character": 4
    },
    "context": {
        "triggerKind": 1
    }
}


Using Prettier v2.8.8 from /Users/user/code/project/node_modules/prettier
[ts] getCompletionData: Get current token: 0.005791962146759033
[ts] getCompletionData: Is inside comment: 0.018958985805511475
[ts] getCompletionData: Get previous token: 0.009042024612426758
[ts] getCompletionsAtPosition: isCompletionListBlocker: 0.1350409984588623
[ts] AutoImportProviderProject: attempted to add more than 10 dependencies. Aborting.
[ts] getExportInfoMap: cache miss or empty; calculating new results
[ts] AutoImportProviderProject: attempted to add more than 10 dependencies. Aborting.
[ts] getExportInfoMap: done in 366.46025002002716 ms
[ts] AutoImportProviderProject: attempted to add more than 10 dependencies. Aborting.
[ts] collectAutoImports: resolved 11050 module specifiers, plus 5786 ambient and 8633 from cache
[ts] collectAutoImports: response is complete
[ts] collectAutoImports: 11070.31395804882
[ts] getCompletionData: Semantic work: 11445.214374959469
[ts] getCompletionsAtPosition: getCompletionEntriesFromSymbols: 101.96841698884964
[Trace - 16:15:43] Received response 'textDocument/completion - (14)' in 43713ms.


@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

Without cache, the response time for completion requests was 43 seconds in a large project.
I think this is not the entire time because the response was logged. It's a JSON of 479 thousand lines, unformatted. That's 21 megabytes. Did all of these 21 megabytes get transmitted over the network?

SnapshotManager File Statistics:
Project files: 8883
Svelte files: 2551
From node_modules: 0
Total: 8883

@jasonlyu123
Copy link
Member

Ok. This is the same issue as #2244 (comment). The issue is because of the giant export map. TypeScript loops through the export map to find the subpath for each symbol so it's an n x n situation. 2000 subpaths take way longer than 800. And the reason for it to be faster in a ts file is because of the " parser error stage" issue I mentioned earlier. In the "parser error stage", the auto-import is the same as triggering from an empty position instead of filtered by what you already typed, IcS.

Is there a reason why you need an export map of this size? This probably needs to be optimised in upstream TypeScript. However, I am hesitant to open an issue because the export map is unusually big.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

Is there any way to improve retrieving completions from an erroneous location? After all, I'm just trying to select a component, and I definitely don't expect it to lead to scanning the entire project.

@jasonlyu123
Copy link
Member

It's a limitation we have, see #776. We could probably add a /> afterwards so the svelte compiler is happy but this will make ts rebuild a lot of things, which is "normally" even more inefficient. The same also applies to "looping through export map", It "normally" should be less efficient to create a hash map to avoid the "n x n" looping. So I would prefer to know why is this needed in the first place before trying to optimise for an edge case that might slow down everyone else.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

I cleaned up exports in package.json. I closed the tag so that the parser wouldn't complain. I excluded a directory with a large number of files in the settings. In my large project, this took 30 seconds. It's still strange.

81f5a7e7-5439-4883-a1ba-ff56b9fd83cf.mp4

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

So I would prefer to know why is this needed in the first place before trying to optimise for an edge case that might slow down everyone else.

What's the point of this? Projects often have many components. Typically, you don't remember their names, especially if the project has been around for a while. In my example, I want to import an icon (or anything else). I type in the first few characters I remember and want to see suggestions, then use them as a reference to remember what I'm looking for. Seems pretty standard to me.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

Over the past 3 years, our project has become very large. And perhaps what irritates our entire team the most is that it has become difficult to work in any IDE. Any of them. Svelte LSP in VS Code consumes all the RAM. In Neovim, everything just hangs, you can forget about Svelte LSP altogether. In WebStorm, it's better, but auto-import doesn't work at all. And that's our main pain point.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

We're already joking (or almost seriously) with each other that it's time to rewrite all of this in Go or Rust.

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

@jasonlyu123

By the way, in the latest video, it's noticeable that after the completion request, nothing happens, there's some waiting. It's unclear what's going on. And then after some time, traces and other logs appear. Any theories?

I've already turned everything off, no ideas.

UPDATE:

I deleted node_modules, but the completion request still took more than 30 seconds. By process of elimination, I began deleting directories from the project and noticed that textDocument/completion started to work faster. I understand that, initially, when I try to go to definition, or hover, or request completions, the project scanning starts. And only after that does the actual request execute. We have 10k files in our project, according to Git. If all these components are still being preprocessed, it's sad.

image

@MrWaip
Copy link
Author

MrWaip commented Apr 5, 2024

image

Okay, I was right. When the LSP receives the first request, it tries to obtain typescript.Program from the TypeScript LSP. And this takes a lot of time. In my large project, it starts processing 8k files, 2500 of which are Svelte components. This takes around 30-40 seconds. But I get the completions themselves quite quickly with the previous optimization.

Do you think it's possible not to load all Svelte components into the TypeScript LSP at once for processing, but to add them as files are opened? Make such a feature experimental for large projects. I would even try it myself if you give me a hint on where to start. Well, 30-40 seconds at startup is quite long, I believe.

File: packages/language-server/src/plugins/typescript/service.ts
Line: 716
const program = languageService.getProgram();

@jasonlyu123
Copy link
Member

So I would prefer to know why is this needed in the first place before trying to optimise for an edge case that might slow down everyone else.

What's the point of this? Projects often have many components. Typically, you don't remember their names, especially if the project has been around for a while. In my example, I want to import an icon (or anything else). I type in the first few characters I remember and want to see suggestions, then use them as a reference to remember what I'm looking for. Seems pretty standard to me.

What I meant is that 12000+ lines of package.json isn't normal. The reason it is slow is because TypeScript loops through the export map for subpath export for each symbol found. If there are 2 exports per file and there are 2000 conditions in your export map. There'll be 2* 2000 * 2000 loops. This needs to be optimised in TypeScript but I currently only see this in svelte icon libraries so I think it would be better to know why this is needed in the first place and solve that problem. Even if it's reported in TypeScript, they might still ask why can't just use the ./* condition instead. It'll also need at least three months for the fix to be released.

It's unclear what's going on. And then after some time, traces and other logs appear. Any theories?
This is probably because the package.json is not being watched for changes so you need to restart the editor for the change to be in effect. It is a situation where we didn't check if package.json is cached.

Do you think it's possible not to load all Svelte components into the TypeScript LSP at once for processing, but to add them as files are opened?

The rootNames are files included in your tsconfig.json. For files to not be included here, you might need to exclude them in the tsconfig.json. You can also try to split your project into smaller packages with .d.ts files for type info. This should allow TypeScript only to parse the type info.

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

You were right. During the startup, processing TypeScript files takes up most of the time for typescript.Program. I roughly calculated the time. All Svelte processing took 8 seconds, while the rest took 31 seconds.

image

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

What I meant is that 12000+ lines of package.json isn't normal. The reason it is slow is because TypeScript loops through the export map for subpath export for each symbol found. If there are 2 exports per file and there are 2000 conditions in your export map. There'll be 2* 2000 * 2000 loops. This needs to be optimised in TypeScript but I currently only see this in svelte icon libraries so I think it would be better to know why this is needed in the first place and solve that problem. Even if it's reported in TypeScript, they might still ask why can't just use the ./* condition instead. It'll also need at least three months for the fix to be released.

I got you. I'll definitely try to optimize the export map of icons.

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

The rootNames are files included in your tsconfig.json. For files to not be included here, you might need to exclude them in the tsconfig.json. You can also try to split your project into smaller packages with .d.ts files for type info. This should allow TypeScript only to parse the type info.

Maybe we'll add everything that isn't used during development to the exclude list but include it in the pipeline.

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

@jasonlyu123 Are there any other optimizations possible? But thank you anyway!

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

Damn. It's not supported(

#2148 (comment)

@jasonlyu123
Copy link
Member

jasonlyu123 commented Apr 6, 2024

Did you mean "used to load different tsconfig that doesn't use the name 'tsconfig.json'" or "Build-Free Editing with Project References"? The latter won't help with start-up time since the source file is still loaded.

@MrWaip
Copy link
Author

MrWaip commented Apr 6, 2024

I mean that

@jasonlyu123 jasonlyu123 removed the bug Something isn't working label Apr 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
perf Performance
Projects
None yet
Development

No branches or pull requests

3 participants