dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.87k stars 782 forks source link

Compiler service: parallel parsing. #14851

Open safesparrow opened 1 year ago

safesparrow commented 1 year ago

Is your feature request related to a problem? Please describe.

IDE interactions in F# projects are slow and they make development experience subpar.

Describe the solution you'd like

One way to speed them up is to allow all files in a project to be parsed in parallel, when type-checking of multiple files/whole project is about to be performed.

Describe alternatives you've considered

Introducing a parsing cache of configurable size, that applies to all endpoints correctly would limit the slowdown caused by slow, sequential parsing. I raised https://github.com/dotnet/fsharp/issues/14848 for that. However:

  1. this doesn't apply to first-time parsing after IDE load
  2. parsing cache can get full. I do think that a cache large-enough to cover all source files in the solution is likely to have a small-enough memory footprint to be acceptable for most users - so hopefully this isn't very relevant.
  3. Every time a project changes, all parsing results get invalidated.

Additional context

majocha commented 1 year ago

Some thoughts. Parsing eagerly without waiting for typecheck is conceptually equivalent to caching every parse. This is basically what #14852 do, but even more aggressive.

After #14852 We could quite easily cache awaitables. Parsing is not cancellable from what I understand, so let's say we store a backgroundTask.

Now if we don't wait for the BoundModel to request a result, but start that task eagerly in SyntaxTree constructor, we're basically done.

One question remains, how compatible this approach is with partial checking.

majocha commented 1 year ago

So basically we could launch parsing tasks in parallel at IncrementalBuilder construction and also on every file changed notification. Cache the tasks and return Task.Result each time the builder calls for parse.

safesparrow commented 1 year ago

The main questions then are:

  1. whether IncrementalBuilder is constructed eagerly for all files in the project or one at a time, waiting for .Parse to finish before proceeding to the next file (easy to verify).
  2. Is it safe to do the parsing in parallel. As I understand, currently there is only one set of project options/project parsing options that ever exist/are used at the same time. With parallel parsing this would no longer be the case, I think.
  3. How much do we want to try and cancel out-of-date requests. Eg. if I open a project and then unload it, should we try to cancel now outdated requests, or is that not very important because parsing is quite quick compared to user actions that can make some parse results out of date?
majocha commented 1 year ago
  1. looks like one IB is constructed per project, it already gets a complete file list that resides in InitialState (does not change). Parsing happens at the type check phase later.
  2. I think it is safe in the current model where cached ParsedInputs lifetime is connected with IB lifetime. How often are IBs destroyed? That's a question.
  3. We can't really cancel a single parsing task at all, from what I understand. I think by the time you unload a project, switch branches etc. there will be no running tasks. What's to look for is the initial impact, opening first file and waiting for typecheck.
majocha commented 1 year ago

@safesparrow I have something that works already and shows some modest gains in benchmarks. It feels OK in the IDE, although I yet have to install it and use for a while.