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

Simplify the computation and caching of 'CompilationWithAnalyzers' for a particular project in the diagnostics layer. #77113

Merged
merged 8 commits into from
Feb 9, 2025

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Feb 7, 2025

Followup to #77111

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Feb 7, 2025
@@ -127,84 +128,6 @@ static string GetLanguageSpecificId(string? language, string noLanguageId, strin
language: language);
}

public static async Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

moved to local function in single place it is needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

i also lifted the 'compilation == null' check below out of it. but it is otherwise unchanged.

/// passed along with the project. As such, we might not be able to use a prior cached value if the set of state
/// sets changes. In that case, a new instance will be created and will be cached for the next caller.
/// </summary>
private static readonly ConditionalWeakTable<Project, CompilationWithAnalyzersPair?> s_projectToCompilationWithAnalyzers = new();
Copy link
Member Author

Choose a reason for hiding this comment

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

changed from WeakRef to CWT. that way we can reuse when asking about other projects as well. and, as long as the project is alive we keep the CompPair (instead of dumping on first GC).

{
private static Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync(Project project, ImmutableArray<StateSet> stateSets, bool crashOnAnalyzerException, CancellationToken cancellationToken)
Copy link
Member Author

Choose a reason for hiding this comment

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

i wanted to hide this method. no one should just make the CompPair. They shoudl go through the helper that returns if we have a cached one in the CWT.

private sealed class ProjectAndCompilationWithAnalyzers
{
public Project Project { get; }
public CompilationWithAnalyzersPair? CompilationWithAnalyzers { get; }
Copy link
Member Author

Choose a reason for hiding this comment

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

instead of a weakref to one of these, we have a CWT from a Project to the last CompPair we computed for it.

{
if (projectAndCompilationWithAnalyzers.CompilationWithAnalyzers == null)
{
return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

note: i'm virtually certain this is a bug. that's because a prior call with a DIFFERENT set of 'state sets' could have caused this to be computed and cached as 'null', while THIS set of 'state sets' should have produced a legal CompPair. But because of this check we would bail out. I've fixed this in the new code.

return box.Value.diagnosticAnalysisResults;

// Otherwise, just compute for the state sets we care about.
var compilation = await GetOrCreateCompilationWithAnalyzersAsync(project, stateSets, Owner.AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false);
Copy link
Member Author

Choose a reason for hiding this comment

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

calls GetOrCreateCompilationWithAnalyzersAsync instead of CreateCompilationXXX. We want this to reuse a cached value if applicable.

static (stateSet, arg) => arg.self.IsCandidateForFullSolutionAnalysis(stateSet.Analyzer, stateSet.IsHostAnalyzer, arg.project),
(self: this, project));

var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

calls GetOrCreateCompilationWithAnalyzersAsync instead of CreateCompilationXXX. We want this to reuse a cached value if applicable.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review February 8, 2025 18:49
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner February 8, 2025 18:49
CancellationToken cancellationToken)
{
if (!project.SupportsCompilation)
return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

lifted this up from CreateCompilationWithAnalyzersAsync

static bool HasAllAnalyzers(ImmutableArray<StateSet> stateSets, CompilationWithAnalyzersPair? compilationWithAnalyzers)
{
if (compilationWithAnalyzers is null)
return false;
Copy link
Member Author

Choose a reason for hiding this comment

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

this check is new, but is also a bug fix. we might have stored 'null' before because we decided to not produce a compilation-pair for a particular set of state-sets. taht doesn't mean we should fail from trying to produce a compilation with our current set.

// Make sure the cached pair was computed with at least the same state sets we're asking about. if not,
// recompute and cache with the new state sets.
if (!s_projectToCompilationWithAnalyzers.TryGetValue(project, out var tupleBox) ||
!stateSets.IsSubsetOf(tupleBox.Value.stateSets))
Copy link
Member Author

Choose a reason for hiding this comment

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

changed to use same subset logic used elsewhere in this component.

s_lastProjectAndCompilationWithAnalyzers.SetTarget(new ProjectAndCompilationWithAnalyzers(project, compilationWithAnalyzers));
return compilationWithAnalyzers;

static bool HasAllAnalyzers(IEnumerable<StateSet> stateSets, CompilationWithAnalyzersPair compilationWithAnalyzers)
Copy link
Member Author

Choose a reason for hiding this comment

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

removed this. instead of checking that the compWithAnalyzers has all the analyzers we're caring about. we just check that the state sets we're asking about are a subset of the last state sets we computed the CompWithAnalyzers for.

@CyrusNajmabadi
Copy link
Member Author

@JoeRobich @dibarbet this is ready for review.


// in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to
// async being used with synchronous blocking concurrency.
var projectAnalyzerOptions = new CompilationWithAnalyzersOptions(
Copy link
Member

Choose a reason for hiding this comment

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

Would it be worthwhile to only construct the options if we knew we were going to use them?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes. that would make sense. this logic exists from before. but i can make that change later!

Copy link
Member Author

Choose a reason for hiding this comment

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

Made in 3c5c686

@CyrusNajmabadi CyrusNajmabadi merged commit 3b7a814 into dotnet:main Feb 9, 2025
25 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Feb 9, 2025
@CyrusNajmabadi CyrusNajmabadi deleted the projectCompilationMap branch February 9, 2025 01:33
async Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync()
{
var projectAnalyzers = stateSets.SelectAsArray(s => !s.IsHostAnalyzer, s => s.Analyzer);
var hostAnalyzers = stateSets.SelectAsArray(s => s.IsHostAnalyzer, s => s.Analyzer);
Copy link
Contributor

Choose a reason for hiding this comment

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

Both these arrays look to just be intermediaries used only once. Could they be removed and the filtered arrays populated something like:

var filteredProjectAnalyzers = stateSets.SelectAsArray(s => !s.IsHostAnalyzer && !s.Analyzer.IsWorkspaceDiagnosticAnalyzer(), s => s.Analyzer);
var filteredHostAnalyzers = stateSets.SelectAsArray(s => s.IsHostAnalyzer && !s.Analyzer.IsWorkspaceDiagnosticAnalyzer(), s => s.Analyzer);


// Intentionally ignore the result of this. We still want to use the value we computed above, even if
// another thread interleaves and sets a different value.
s_projectToCompilationWithAnalyzers.GetValue(project, _ => tupleBox);
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there an AddOrUpdate on CWT?

/// passed along with the project. As such, we might not be able to use a prior cached value if the set of state
/// sets changes. In that case, a new instance will be created and will be cached for the next caller.
/// </summary>
private static readonly ConditionalWeakTable<Project, StrongBox<(ImmutableArray<StateSet> stateSets, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new();
Copy link
Contributor

@ToddGrun ToddGrun Feb 10, 2025

Choose a reason for hiding this comment

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

StrongBox

Out of curiosity, why StrongBox instead of just using Tuple directly?

@@ -7,6 +7,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants