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

[Tooling Optimization] Skip background type-checking on implementation files if signature files exist #10199

Merged
merged 34 commits into from
Oct 6, 2020

Conversation

TIHan
Copy link
Contributor

@TIHan TIHan commented Sep 28, 2020

Well over a year ago, I experimented with an optimization trick that sped up tooling significantly. Here, I'm trying to see if it's worth actually getting it in.

When editing then saving files, we always have to re-type-check everything - sometimes entire project(s) even on a single file change. This means we are constantly building a lot of large structures and throwing them away in a short amount of time. This takes a lot of CPU time and memory.

The optimization basically stops the background type-checking of implementation files if a backing signature(.fsi) file exists. This should be safe to do as the signature file will always be the public-facing API over the implementation file; therefore, the implementation file doesn't matter in most cases. You will still have to type-check an implementation file if you open it, or perform a find-all references.

The optimization only affects projects who use signature files. But for FSharp.Compiler.Private, it gives a very significant speed up in order to get colorization, tool-tips, etc.

The following are some results from starting up VisualFSharp.sln:

  • FSharp.Compiler.Private - service.fs
    • Without Optimization: 60-65 seconds to colorization
    • With Optimization: 18-23 seconds to colorization
  • FSharp.Editor - FSharpProjectOptionsManager.fs
    • Without Optimization: 62-67 seconds to colorization
    • With Optimization: 18-25 seconds to colorization

This is very significant. At the very least, it would boost tooling performance when working in the compiler.

Remaining and currently known tasks:

  • Lazily type-check implementation files when we perform a find-all references.
  • (Stretch) Lazily parse implementation files - extra perf gain; if we don't have to type-check it, we might not have to parse it.
  • (Stretch) Prevent parse/type-check of other files when modifying implementation files with backing signature files; allows modifying implementation files without ever having to analyze any other file or project. Skip parse/type-check of other files when modifying implementation files with backing signature files #10210 Will make a separate PR for that change, it's the inverse of this optimization.

Implementation details:

  • Adds a new flag to FSharpChecker, enablePartialTypeChecking - this will enable the optimization described above.

@TIHan TIHan changed the title [WIP][Tooling Optimization] Stop background type-checking implementation files if signature file exists [WIP][Tooling Optimization] Stop background type-checking on implementation files if signature files exist Sep 28, 2020
@saul
Copy link
Contributor

saul commented Sep 29, 2020

This would be absolutely brilliant to get merged in - we make heavy use of signature files in our libraries and would love to see how this improves our IDE experience.

@TIHan
Copy link
Contributor Author

TIHan commented Sep 29, 2020

@saul, here is a VSIX you can try out: VisualFSharpFull.zip

The state of the current change does what it is intended to do; it should give you an indication of how well it improves your experience. Some tooling will not work, such as find-all refs, but I think go-to-def should still be functional.

@TIHan TIHan changed the title [WIP][Tooling Optimization] Stop background type-checking on implementation files if signature files exist [WIP][Tooling Optimization] Prevent background type-checking on implementation files if signature files exist Sep 29, 2020
@TIHan TIHan changed the title [WIP][Tooling Optimization] Prevent background type-checking on implementation files if signature files exist [WIP][Tooling Optimization] Skip background type-checking on implementation files if signature files exist Oct 2, 2020
@TIHan TIHan linked an issue Oct 2, 2020 that may be closed by this pull request
@TIHan TIHan linked an issue Oct 2, 2020 that may be closed by this pull request
@TIHan TIHan changed the title [WIP][Tooling Optimization] Skip background type-checking on implementation files if signature files exist [Tooling Optimization] Skip background type-checking on implementation files if signature files exist Oct 2, 2020
@TIHan
Copy link
Contributor Author

TIHan commented Oct 2, 2020

@jonsequitur @cartermp I can say that this is done now.

Copy link
Contributor

@cartermp cartermp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall approach seems reasonable. Just a quick check - do the following work without issue?

  • Find Refs
  • Rename
  • Autocompletion
  • Lightbulb suggesting names after a misspelling
  • Add open code fix to something that was defined in another file or project
  • etc.

If there's high confidence in this kind of stuff then I think this is good. I'm eager to get it into dogfood builds to evaluate it across a larger codebase.

@@ -687,75 +709,90 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC
/// Fetch the check information from the background compiler (which checks w.r.t. the FileSystem API)
member __.GetBackgroundCheckResultsForFileInProject(filename, options, userOpName) =
reactor.EnqueueAndAwaitOpAsync(userOpName, "GetBackgroundCheckResultsForFileInProject", filename, fun ctok ->
cancellable {
cancellable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this tabbing over now makes the inner scope the same, which looks worse :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I see why.

@@ -1022,16 +1023,91 @@ module IncrementalBuilderEventTesting =

module Tc = FSharp.Compiler.TypeChecker

[<AutoOpen>]
module IncrementalBuildSyntaxTree =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is internal, but I think there needs to be some comments explaining why we need our own abstraction over a syntax tree for the incremental builder. If I were to need to change something in here, I'd have nothing to look at for context on why this exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some kind of object type that wraps the information to lazily parse the source text to get a ParsedInput(which is the root of the syntax tree). The name SyntaxTree feels correct here.


/// Accumulated results of type checking.
[<Sealed>]
type SyntaxTree (tcConfig: TcConfig, fileParsed: Event<string>, lexResourceManager, sourceRange: range, filename: string, isLastCompiland) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be private?

Copy link
Contributor Author

@TIHan TIHan Oct 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. We need access to SyntaxTree when the enclosing module is auto-opened.

I actually do not want the IncrementalBuildSyntaxTree module; instead we should just define SyntaxTree without putting in an auto-opened module. The reason why we have this module is because SyntaxTree is already a module in the compiler, so I can't define the type with that name unless I put it in another module, and then open that.

Copy link
Member

@KevinRansom KevinRansom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the change, and it looks good. I have some style questions and naming nonsense. But other than that it looks great.

let! topAttrs, implFile, _implFileHiddenType, tcEnvAtEnd, createsGeneratedProvidedTypes =
TypeCheckOneImplFile (tcGlobals, tcState.tcsNiceNameGen, amap, tcState.tcsCcu, checkForErrors, conditionalDefines, tcSink, tcConfig.internalTestSpanStackReferring) tcImplEnv rootSigOpt file
let typeCheckOne =
if skipImplIfSigExists && hadSig then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, is hadsig the right name? It seems as if it should be hasSig.

Copy link
Contributor Author

@TIHan TIHan Oct 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hadSig refers to if the impl file "had" a sig before we type-checked. hasSig might mean if the impl has any kind of signature, whether it be generated after a type-check or from a '.fsi' file.

Honestly, hasSig might also be correct depending on how you look at it.

The most accurate name would be hasSigFromSigFile.

@@ -1022,16 +1023,91 @@ module IncrementalBuilderEventTesting =

module Tc = FSharp.Compiler.TypeChecker

[<AutoOpen>]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoOpen really? seems a bit broad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say it's broad; there is only one item in the module, which is SyntaxTree.

return state.Partial
}

member this.TcInfoFull =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not clear to me what this name means? the value gets an unnamed tuple of TcInfo, TcInfoOptional.

perhaps the name should be this.TcInfoAndTcInfoOptional ... which is yuk!!! but at least it tells us what we are getting.

oh I don't know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, this name is not great, but I'm struggling to figure out what I should call it.

Basically, whenever I try to access this property, it's going to try to type-check the file to get the full and rich information that would be needed for tooling. TcInfoAndTcInfoOptional is accurate and might be better though.

let parseResults = FSharpParseFileResults(creationErrors, None, true, [| |])
let typedResults = FSharpCheckFileResults.MakeEmpty(filename, creationErrors, reactorOps, keepAssemblyContents)
return (parseResults, typedResults)
| Some builder ->
let! (parseTreeOpt, _, _, untypedErrors) = builder.GetParseResultsForFile (ctok, filename)
let! tcProj = builder.GetCheckResultsAfterFileInProject (ctok, filename)

let tcInfo, tcInfoOptional = tcProj.TcInfoFull ctok
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you determine whether to space separate functions or have them right next to each other? I struggle with this too. I wondered why line 742, 733 and 735 are empty lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the call TcInfoFull is really important to see clearly as it can do a lot of computation depending on if enablePartialTypeChecking in FSharpChecker is set to true.

Copy link
Contributor Author

@TIHan TIHan Oct 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 733/735 - the spaces should be removed.

let! (tcProj, ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt) = builder.GetCheckResultsAndImplementationsForProject(ctok)
let errorOptions = tcProj.TcConfig.errorSeverityOptions
let fileName = TcGlobals.DummyFileNameForRangesWithoutASpecificLocation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

818 and 820 are empty, the remainder not, I wondered why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning I mentioned above.

@TIHan
Copy link
Contributor Author

TIHan commented Oct 6, 2020

@cartermp All tooling should still work as before. There is a performance difference with find-all refs as we now have to re-type-check files that only have partial data(or no symbols recorded).

When this optimization is enabled, background type-checking will not record any symbols on a file, making it partial. We do this to avoid extra work when it isn't needed.

@TIHan TIHan merged commit 6bb2c07 into dotnet:main Oct 6, 2020
nosami pushed a commit to xamarin/visualfsharp that referenced this pull request Feb 23, 2021
…n files if signature files exist (dotnet#10199)

* Initial work to enable FSI optimizations in tooling

* Remove extra type-check function

* Not working but initial work to lazily eval

* some work

* simplify

* simplify again

* Using Async

* work

* work

* builds

* almost there

* Minor change

* minor refactor

* working, but not quite optimized

* Fix dependency files

* Refactor types

* Added 'enableLazyTypeChecking'.

* Enable lazy type checking in VS

* Added documentation

* Added SyntaxTreeAccumulator

* prevent parsing impl files if possible

* Trying to fix tests

* Refactor

* Throw if enablePartialChecking and keepAssemblyContents are both enabled

* minor comment update

* minor refactor

* Trying to pass tests

* Only getting assembly data

* Remove unnecessary change

* Added some documentation

* Minor comment update

* Renamed quickCheck to skipImplIfSigExists

* Minor update

* Feedback
@TIHan TIHan mentioned this pull request Aug 16, 2021
10 tasks
nosami pushed a commit to xamarin/visualfsharp that referenced this pull request Jan 26, 2022
…n files if signature files exist (dotnet#10199)

* Initial work to enable FSI optimizations in tooling

* Remove extra type-check function

* Not working but initial work to lazily eval

* some work

* simplify

* simplify again

* Using Async

* work

* work

* builds

* almost there

* Minor change

* minor refactor

* working, but not quite optimized

* Fix dependency files

* Refactor types

* Added 'enableLazyTypeChecking'.

* Enable lazy type checking in VS

* Added documentation

* Added SyntaxTreeAccumulator

* prevent parsing impl files if possible

* Trying to fix tests

* Refactor

* Throw if enablePartialChecking and keepAssemblyContents are both enabled

* minor comment update

* minor refactor

* Trying to pass tests

* Only getting assembly data

* Remove unnecessary change

* Added some documentation

* Minor comment update

* Renamed quickCheck to skipImplIfSigExists

* Minor update

* Feedback
nosami pushed a commit to xamarin/visualfsharp that referenced this pull request Jan 26, 2022
…n files if signature files exist (dotnet#10199)

* Initial work to enable FSI optimizations in tooling

* Remove extra type-check function

* Not working but initial work to lazily eval

* some work

* simplify

* simplify again

* Using Async

* work

* work

* builds

* almost there

* Minor change

* minor refactor

* working, but not quite optimized

* Fix dependency files

* Refactor types

* Added 'enableLazyTypeChecking'.

* Enable lazy type checking in VS

* Added documentation

* Added SyntaxTreeAccumulator

* prevent parsing impl files if possible

* Trying to fix tests

* Refactor

* Throw if enablePartialChecking and keepAssemblyContents are both enabled

* minor comment update

* minor refactor

* Trying to pass tests

* Only getting assembly data

* Remove unnecessary change

* Added some documentation

* Minor comment update

* Renamed quickCheck to skipImplIfSigExists

* Minor update

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