Skip to content

Remove potential leak in CachingSemanticModelProvider._providerCache - #77192

Merged
ToddGrun merged 3 commits into
dotnet:mainfrom
ToddGrun:CWT_In_CachingSemanticModelProvider_Leak
Feb 14, 2025
Merged

Remove potential leak in CachingSemanticModelProvider._providerCache#77192
ToddGrun merged 3 commits into
dotnet:mainfrom
ToddGrun:CWT_In_CachingSemanticModelProvider_Leak

Conversation

@ToddGrun

Copy link
Copy Markdown
Contributor

Anders recently shared a dmp where he was complaining about excessive memory usage in the Roslyn OOP process. Even after turning off FSA, he was still experiencing relatively high memory usage. Inspecting the dmp led to some questionable data in the CachingSemanticModelProvider._providerCache CWT.

Luckily, Dustin was on the thread and mentioned a known issue in the CLR around CWTs (dotnet/runtime#12255). This is indeed the case we are hitting, as the CWT's value could container a cycle with a pointer to itself (CachingSemanticModelProvider._providerCache -> PerCompilationProvider -> Compilation -> CachingSemanticModelProvider). The standard fix for this is to switch the CWT to static, and thus can't experience this cycle.

This PR simply changes that CWT to static (and ensures that only a single CachingSemanticModelProvider is in use as it only uses static data to calculate it's result).

Note that I kept in the ClearCache mechanism in this PR, even though it seems quite broken. Failure to call the ClearCache(compilation) method would have led to this leak, and indeed, this is what I experience when debugging locally.

Anders recently shared a dmp where he was complaining about excessive memory usage in the Roslyn OOP process. Even after turning off FSA, he was still experiencing relatively high memory usage. Inspecting the dmp led to some questionable data in the CachingSemanticModelProvider._providerCache CWT.

Luckily, Dustin was on the thread and mentioned a known issue in the CLR around CWTs (dotnet/runtime#12255). This is indeed the case we are hitting, as the CWT's value could container a cycle with a pointer to itself (CachingSemanticModelProvider._providerCache -> PerCompilationProvider -> Compilation -> CachingSemanticModelProvider). The standard fix for this is to switch the CWT to static, and thus can't experience this cycle.

This PR simply changes that CWT to static (and ensures that only a single CachingSemanticModelProvider is in use as it only uses static data to calculate it's result).

Note that I kept in the ClearCache mechanism in this PR, even though it seems quite broken. Failure to call the ClearCache(compilation) method would have led to this leak, and indeed, this is what I experience when debugging locally.
@ToddGrun
ToddGrun requested a review from a team as a code owner February 12, 2025 22:13
@ghost ghost added Area-Analyzers untriaged Issues and PRs which have not yet been triaged by a lead labels Feb 12, 2025

@333fred 333fred left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we have a bug to follow up on the general semantics of this type and either make it entirely static, or otherwise fix the issues with it?

@ToddGrun

Copy link
Copy Markdown
Contributor Author

I didn't log anything.

As there is only one type that derives from SemanticModelProvider (CachingSemanticModelProvider), I considered condensing down to just a single class and making it static, but that seemed orthogonal to the memory leak. I don't think the current design is necessarily bad, it's just a bit more complex than it needs to be.

Another concern is that ClearCache(Compilation) doesn't seem to be getting called, which sounds like an intentional decision. If we wish to maintain that, perhaps ClearCache(Compilation) should be removed so it doesn't claim to do cleanup that doesn't happen.


In reply to: 2613565789

@333fred

333fred commented Feb 13, 2025

Copy link
Copy Markdown
Member

I didn't log anything.

Let's please log something to follow up.

@ToddGrun

Copy link
Copy Markdown
Contributor Author

Let's please log something to follow up.

#77220

@ToddGrun

Copy link
Copy Markdown
Contributor Author

@dotnet/roslyn-compiler for 2nd review

= new ConditionalWeakTable<Compilation, PerCompilationProvider>.CreateValueCallback(compilation => new PerCompilationProvider(compilation));

private readonly ConditionalWeakTable<Compilation, PerCompilationProvider> _providerCache;
private static readonly ConditionalWeakTable<Compilation, PerCompilationProvider> s_providerCache = new ConditionalWeakTable<Compilation, PerCompilationProvider>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this made static? Couldn't it remain an instance field with the same effect since the owner is a singleton now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The general guidance per @DustinCampbell is that CWT declarations should be static if possible to remove the cyclic leak possibility. But yes, it doesn't need to be static now that the owner is a singleton, but I figured it was best to do so.

/// </summary>
internal sealed class CachingSemanticModelProvider : SemanticModelProvider
{
public static CachingSemanticModelProvider Instance { get; } = new CachingSemanticModelProvider();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider adding a comment linking to the CWT issue so someone doesn't unintentionally undo this change in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Analyzers untriaged Issues and PRs which have not yet been triaged by a lead

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants