Skip to content

Conversation

@ToddGrun
Copy link
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
Copy link
Member

@333fred 333fred left a comment

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
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
Copy link
Member

333fred commented Feb 13, 2025

I didn't log anything.

Let's please log something to follow up.

@ToddGrun
Copy link
Contributor Author

Let's please log something to follow up.

#77220

@ToddGrun
Copy link
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
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
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
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.

@ToddGrun ToddGrun enabled auto-merge (squash) February 14, 2025 13:21
@ToddGrun ToddGrun merged commit d993eb9 into dotnet:main Feb 14, 2025
24 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Feb 14, 2025
@akhera99 akhera99 modified the milestones: Next, 17.14 P2 Feb 25, 2025
@ToddGrun ToddGrun deleted the CWT_In_CachingSemanticModelProvider_Leak branch March 5, 2025 18:05
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