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

Transparent Compiler memory reduction in editors #17543

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

TheAngryByrd
Copy link
Contributor

@TheAngryByrd TheAngryByrd commented Aug 15, 2024

Description

This fixes #16979 with some additions that have been discussed in various chats/twitter threads. I've combined them for discussion purposes, we can split them up as desired.

The 3 parts are:

  1. Makes the snapshots -> options use a ConditionalWeakTable rather than recreating possibly hundreds of options objects.
  2. Adds many various knobs to the transparent compiler caching code, allowing tweaking for editors.
  3. Adds FsAutocomplete to the list of InternalsVisibleTo so we can utilize these cache tweaking changes.
    • FCS has never bee backwards compatible and FSAC has always had to deal with source level changes so any concerns around versioning wouldn't really apply here. We're used to it 😄

❓ Outstanding questions ❓

  • What type of benchmarking should I do for this?
    • I could show before/after on allocations in FSAC.
    • Add a benchmark dotnet scenario but I'm unsure how big a project graph needs to be before this exhibits the behavior.

Checklist

  • Test cases added

  • Performance benchmarks added in case of performance changes

  • Release notes entry updated:

    Please make sure to add an entry with short succinct description of the change as well as link to this pull request to the respective release notes file, if applicable.

    Release notes files:

    • If anything under src/Compiler has been changed, please make sure to make an entry in docs/release-notes/.FSharp.Compiler.Service/<version>.md, where <version> is usually "highest" one, e.g. 42.8.200
    • If language feature was added (i.e. LanguageFeatures.fsi was changed), please add it to docs/releae-notes/.Language/preview.md
    • If a change to FSharp.Core was made, please make sure to edit docs/release-notes/.FSharp.Core/<version>.md where version is "highest" one, e.g. 8.0.200.

    Information about the release notes entries format can be found in the documentation.
    Example:

    If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

Copy link
Contributor

github-actions bot commented Aug 15, 2024

❗ Release notes required

@TheAngryByrd,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

* <Informative description>. ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

You can open this PR in browser to add release notes: open in github.dev

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/9.0.100.md No release notes found or release notes format is not correct

@TheAngryByrd TheAngryByrd changed the title Snapshot fixes Transparent Compiler memory reduction in editors Aug 15, 2024
@@ -258,45 +258,119 @@ module private TypeCheckingGraphProcessing =

return finalFileResults, state
}

type internal CompilerCaches(sizeFactor: int) =
type CacheSizes = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably should be internal

Copy link
Contributor Author

@TheAngryByrd TheAngryByrd Aug 15, 2024

Choose a reason for hiding this comment

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

This allows an editor to finely tweak these options. This potentially trades memory for CPU usage but on larger solutions it a tradeoff someone can make it possible to have multiple solutions open.

Copy link
Contributor

Choose a reason for hiding this comment

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

So, how would the editor come up with these numbers? I wouldn't suggest to expose this to the user.

I think you should be able to say "use more memory" or "use less memory", potentially even "hold more/less stuff strongly/weakly" but does it really make sense to have such granular control in the editor? So that different editors would benefit from different configurations somehow?

And if there is some smart way to set these based the solutions the user is working here it would be nice to have that code here so everyone can use it.

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 exposed these numbers so it was easier to try to adjust them individually but I do agree it's not something I'd want to expose to a user directly.

I think the only settings come down to "memory" vs "recacluating". Specifically, my work project can be around 10-15gb once fully type checked, if I have multiple versions of open for various reasons, I'd rather try to be aggressive with collecting and keeping it's profile small.

Copy link
Contributor

@0101 0101 Sep 16, 2024

Choose a reason for hiding this comment

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

I guess we can expose it now for easier experimenting with some [<Experimental>] comment that it will be removed/simplified for sure. What I initially put there was pretty arbitrary but hopefully we can eventually come to some consensus here and expose only a few parameters that will be enough to tweak it.

@@ -559,7 +559,7 @@ and [<Experimental("This FCS API is experimental and subject to change.")>] FSha
referencedProjects: FSharpReferencedProjectSnapshot list,
isIncompleteTypeCheckEnvironment: bool,
useScriptResolutionRules: bool,
loadTime: DateTime,
loadTime: DateTime,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why the space?

Comment on lines +87 to +88
<InternalsVisibleTo Include="FsAutoComplete.Core" />
<InternalsVisibleTo Include="FsAutoComplete" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The FSAC InternalsVisibleTo to allow the cache tweaks

Copy link
Contributor

Choose a reason for hiding this comment

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

This feels very wrong, is this a temporary thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The transparent compiler itself is marked internal and unless the F# team wants to make it public, this is a better method than forcing FSAC to do reflection all the time.

Copy link
Contributor

@nojaf nojaf Aug 16, 2024

Choose a reason for hiding this comment

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

Yeah, that's true, but there's actually a way to enable it using FSharpChecker.Create(useTransparentCompiler = true). Shouldn't more things be driven by that?

I just think this approach feels like a workaround that avoids proper communication, which seems like a missed opportunity. But if the team doesn't see it that way and everyone's fine with it, then I suppose it's okay.

One other thing that concerns me is that this could potentially be a common request for every FCS user.

Copy link
Member

@vzarytovskii vzarytovskii Aug 16, 2024

Choose a reason for hiding this comment

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

I wonder if we should expose everything before we stabilize it? Or, we can expose necessary parts, but put Experimental on them. Or expose them via the checker?

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 don't have a strong preference here. I went with the least invasive changes first to do exactly this, facilitate discussion :)

One other thing that concerns me is that this could potentially be a common request for every FCS user.

Yeah I agree here. Though, it would be nice to also be able to access other things like LanguageFeatures instead of our hack

I wonder if we should expose everything before we stabilize it? We can expose necessary parts, but put Experimental on them. Or expose them via the checker?

Since FCS makes breaking changes that FSAC needs to handle anyway, it's all kind of the same thing in the end to me. Whatever makes more sense for your team.

Copy link
Member

Choose a reason for hiding this comment

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

We'd be happy to have such InternalsVisibleTo entries too in Rider plugin. 🙂
We're also used to the changes in APIs.

Copy link
Member

Choose a reason for hiding this comment

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

In this case we should just expose everything needed via APIs.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would also propose to add [Experimental] APIs for everything you need. Then at least we see everything that's needed when deciding on a stable API.

Comment on lines +679 to +701
let internal snapshotTable = ConditionalWeakTable<ProjectSnapshot, FSharpProjectOptions>()

let rec internal snapshotToOptions (projectSnapshot: ProjectSnapshot) =
snapshotTable.GetValue(projectSnapshot, fun projectSnapshot ->
{
ProjectFileName = projectSnapshot.ProjectFileName
ProjectId = projectSnapshot.ProjectId
SourceFiles = projectSnapshot.SourceFiles |> Seq.map (fun x -> x.FileName) |> Seq.toArray
OtherOptions = projectSnapshot.CommandLineOptions |> List.toArray
ReferencedProjects =
projectSnapshot.ReferencedProjects
|> Seq.map (function
| FSharpReference(name, opts) -> FSharpReferencedProject.FSharpReference(name, opts.ProjectSnapshot |> snapshotToOptions)
| PEReference(getStamp, reader) -> FSharpReferencedProject.PEReference(getStamp, reader)
| ILModuleReference(name, getStamp, getReader) -> FSharpReferencedProject.ILModuleReference(name, getStamp, getReader))
|> Seq.toArray
IsIncompleteTypeCheckEnvironment = projectSnapshot.IsIncompleteTypeCheckEnvironment
UseScriptResolutionRules = projectSnapshot.UseScriptResolutionRules
LoadTime = projectSnapshot.LoadTime
UnresolvedReferences = projectSnapshot.UnresolvedReferences
OriginalLoadReferences = projectSnapshot.OriginalLoadReferences
Stamp = projectSnapshot.Stamp
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the main fix around creating too many options

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes sense. Out of curiosity what are you using the resulting options for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There seems to be paths that still want a FSharpProjectOptions. Following the current use of snapshotToOptions should show them.

Specifically they show up in transparent compiler which get turned into longer lived objects here:

projectSnapshot.ToOptions(),

projectSnapshot.ToOptions())

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see, didn't notice these usages. Wonder what would be a good way to phase them out of there... But for now at least your fix improves it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that would the real fix. I did do before that but it was more invasive.

@vzarytovskii
Copy link
Member

I could show before/after on allocations in FSAC.

Yeah, that's good enough. Also, if possible, CPU consumption.

@@ -258,45 +258,119 @@ module private TypeCheckingGraphProcessing =

return finalFileResults, state
}

type internal CompilerCaches(sizeFactor: int) =
type CacheSizes = {
Copy link
Contributor

Choose a reason for hiding this comment

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

So, how would the editor come up with these numbers? I wouldn't suggest to expose this to the user.

I think you should be able to say "use more memory" or "use less memory", potentially even "hold more/less stuff strongly/weakly" but does it really make sense to have such granular control in the editor? So that different editors would benefit from different configurations somehow?

And if there is some smart way to set these based the solutions the user is working here it would be nice to have that code here so everyone can use it.

static member Default =
let sizeFactor = 100
CacheSizes.Create sizeFactor
type internal CompilerCaches(sizeFactor: CacheSizes) =

let sf = sizeFactor
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be renamed if we keep it like this.

Comment on lines +87 to +88
<InternalsVisibleTo Include="FsAutoComplete.Core" />
<InternalsVisibleTo Include="FsAutoComplete" />
Copy link
Contributor

Choose a reason for hiding this comment

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

I would also propose to add [Experimental] APIs for everything you need. Then at least we see everything that's needed when deciding on a stable API.

@@ -1363,8 +1440,8 @@ type internal TransparentCompiler
node,
(fun tcInfo ->

if tcInfo.stateContainsNodes |> Set.contains fileNode then
failwith $"Oops!"
// if tcInfo.stateContainsNodes |> Set.contains fileNode then
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an unrelated fix. Not sure if it's been discussed elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Been discussed in slack before (probably beyond any history now 😢 ). Yeah this could be reverted but I've seen this popup crashing the TC hard.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. It seems a bit fishy. I need to understand if this is something that should legitimately be happening, clearly previously I thought it shouldn't 😅

Comment on lines +679 to +701
let internal snapshotTable = ConditionalWeakTable<ProjectSnapshot, FSharpProjectOptions>()

let rec internal snapshotToOptions (projectSnapshot: ProjectSnapshot) =
snapshotTable.GetValue(projectSnapshot, fun projectSnapshot ->
{
ProjectFileName = projectSnapshot.ProjectFileName
ProjectId = projectSnapshot.ProjectId
SourceFiles = projectSnapshot.SourceFiles |> Seq.map (fun x -> x.FileName) |> Seq.toArray
OtherOptions = projectSnapshot.CommandLineOptions |> List.toArray
ReferencedProjects =
projectSnapshot.ReferencedProjects
|> Seq.map (function
| FSharpReference(name, opts) -> FSharpReferencedProject.FSharpReference(name, opts.ProjectSnapshot |> snapshotToOptions)
| PEReference(getStamp, reader) -> FSharpReferencedProject.PEReference(getStamp, reader)
| ILModuleReference(name, getStamp, getReader) -> FSharpReferencedProject.ILModuleReference(name, getStamp, getReader))
|> Seq.toArray
IsIncompleteTypeCheckEnvironment = projectSnapshot.IsIncompleteTypeCheckEnvironment
UseScriptResolutionRules = projectSnapshot.UseScriptResolutionRules
LoadTime = projectSnapshot.LoadTime
UnresolvedReferences = projectSnapshot.UnresolvedReferences
OriginalLoadReferences = projectSnapshot.OriginalLoadReferences
Stamp = projectSnapshot.Stamp
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes sense. Out of curiosity what are you using the resulting options for?

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

Successfully merging this pull request may close these issues.

Transparent Compiler: High memory usage in FSAC
5 participants