Skip to content

Conversation

@DustinCampbell
Copy link
Member

@DustinCampbell DustinCampbell commented Mar 25, 2025

Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2403339 (https://developercommunity.visualstudio.com/t/Razor-Still-Struggles-with-Memory-Utiliz/10862202)

This pull request brings back the behavior of weakly holding onto Razor generator output. While holding onto the RazorCodeDocument works well in many cases, there are cases where document snapshots are held onto for a long time, resulting in an effective memory leak. The worst leak is caused by the DynamicFIleInfo system. Razor provides a TextLoader and various document services to Roslyn via DynamicFileInfo that hold onto a DocumentSnapshot. And, because the DocumentSnapshot.Project has a reference to the ProjectSnapshot, the entire project tree of documents is kept alive.

  • Use a WeakReference<RazorCodeDocument> in GeneratedOutputSource.
  • Move GeneratedOutputSource from DocumentState to DocumentSnapshot.
  • Don't force compilation in BackgroundDocumentGenerator before updating DynamicFileInfo. Compilation will happen when requested via the TextLoader by Roslyn.
  • Ensure the ProjectSnapshot sets the capacity when creating its map of DocumentSnapshots, since we know exactly how many documents it will contain.

Initial Changes (3/24/2025)

CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2671111&view=results
Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/621987

Update (3/25/2025)

CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2671715&view=results
Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/622255

Update (3/27/2025)

CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2673040&view=results
Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/622895

Final(?) Update (3/27/2025)

CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2673386&view=results
Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/623124

This change brings back the behavior to hold the generated RazorCodeDocument produced by a document snapshot in a WeakReference. This avoids memory retention problems when document snapshots are rooted in the Roslyn workspace by the DynamicFileInfo system.
Now that GeneratedOutputSource holds its RazorCodeDocument in a WeakReference, there's not much point in keeping it on DocumentState. Instead, it can move to DocumentSnapshot, which is really more appropriate since GeneratedOutputSource needed a DocumentSnapshot to perform computation.
The BackgroundDocumentGenerator doesn't need to force compilation before updating the DynamicFileInfo system with a document snapshot. When the Roslyn workspace receives the DynamicFileInfo, compilation will occur through the TextLoader.
ProjectSnapshot.TryGetDocument(...) turns out to be a significant source of dictionary growth. However, we always know the number of documents in a particular ProjectSnapshot from its ProjectState, so we can set the capacity up front.
@DustinCampbell DustinCampbell requested a review from a team as a code owner March 25, 2025 00:13
Copy link
Contributor

@ryzngard ryzngard left a comment

Choose a reason for hiding this comment

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

:shipit:

@DustinCampbell
Copy link
Member Author

DustinCampbell commented Mar 25, 2025

cc @phil-allen-msft for awareness. Phil, I suspect you'll want to pull this change into the next 17.14 preview.

Update (3/25/2025)

The Good News

The test insertion shows several improvements, which is exciting! It also fixes the Razor completion test, which regressed in January after PR #11320 was merged.

Test Scenario Counter Diff
NavigationTest.OrchardCoreGoToImplementation 9990.Totals CLR_BytesAllocated_devenv 👍Improved: -858,468,371 Bytes (-12.95%)
NavigationTest.OrchardCoreFindAllReferences 9990.Totals CLR_BytesAllocated_devenv 👍Improved: -1,149,137,645 Bytes (-17.35%)
NavigationTest.OrchardCoreFindAllReferences 9990.Totals CLR_BytesAllocated_NonDevenv 👍Improved: -2,117,381,754 Bytes (-54.78%)
ProjectSystemTest.OrchardCoreBuild 9990.Totals CLR_BytesAllocated_NonDevenv 👍Improved: -4,825,298,914 Bytes (-15.77%)
RazorTests.Completion 0100.Completion of an import Duration_TotalElapsedTime 👍Improved: -4,847 ms (-3.46%)
LightBulbInvocationTest.LightBulbInvocationTestForCSharp 0100.None priority Duration_TotalElapsedTime 👍Improved: -2,006 ms (-13.25%)

The Bad News

Unfortunately, this change also introduces a significant regression that I need to look at and solve before this can merge.

Test Scenario Counter Diff
SolutionExplorerSearchPerfTests.TestSolutionExplorerFullSearch 9990.Totals CLR_BytesAllocated_devenv 👎 Regressed: 576,239,994 Bytes (11.04%)

image

For what it's worth, this regression really just brings that test back to what it was reporting before PR #11320. Here's what the last six months look like:

image

So, it could be argued that we should take the regression to get the improvements. However, I've got a few ideas to try first to see if I can fix the regression and (hopefully) keep the improvements.

In addition, note that the regression is in "9990.Totals", which tracks the entire test and not the scenarios. This test includes two scenarios: "Load solution (arctic)", and "0100. Full Search in Solution Explorer", but neither of those scenarios indicates a regression. 🤔

@DustinCampbell
Copy link
Member Author

Update (3/27/2025)

I attempted to fix the SolutionExplorerSearchPerfTests.TestSolutionExplorerFullSearch regression but it only improved it a bit and erased other wins. Given that, I've rolled this PR back to what are essentially the changes that were already reviewed by @davidwengier and @ryzngard.

@DustinCampbell
Copy link
Member Author

I've grabbed a couple of memory snapshot screenshots to demonstrate the memory savings. I opened two common Razor solutions and waited for them to load: OrchardCore and MudBlazor. There are two screenshots for each solution: one without the fix where Razor objects dominate the heap, and one with the fix that shows Razor objects are barely present.

OrchardCore - No Fix

OrchardCore - no fix

OrchardCore - Fixed

OrchardCore - with fix

MudBlazor - No Fix

Mudblazer - no fix

MudBlazor - Fixed

Mudblazer - with fix

@DustinCampbell
Copy link
Member Author

I chatted with @phil-allen-msft, and we're going to go ahead and merge this. It potentially adds more churn to the effort to get the next VS insertion for Razor merged. However, only one regression is expected related to this change, and the improvements are very strong.

@DustinCampbell DustinCampbell merged commit 6f3f8fe into dotnet:main Mar 28, 2025
15 of 17 checks passed
@DustinCampbell DustinCampbell deleted the fix-memory-leaks branch March 28, 2025 17:52
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Mar 28, 2025
@jjonescz jjonescz modified the milestones: Next, 17.14 P3 Apr 1, 2025
DustinCampbell added a commit that referenced this pull request Apr 1, 2025
While working on #11655, I noticed that `TagHelperBinder` accounts for a
surprising amount of memory. This pull request aims to clean it up,
reduce the amount of memory it holds onto, and improve the performance
of binding operations. I recommend reviewing commit-by-commit as most
commits have descriptions to aid reviewers.

CI Build:
https://dev.azure.com/dnceng/internal/_build/results?buildId=2675893&view=results
Test Insertion:
https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/623866
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants