Skip to content

Simplify caching api between CodeAnalysisService and DiagnosticAnalysisService#77094

Merged
CyrusNajmabadi merged 12 commits intodotnet:mainfrom
CyrusNajmabadi:diagnosticCaching
Feb 7, 2025
Merged

Simplify caching api between CodeAnalysisService and DiagnosticAnalysisService#77094
CyrusNajmabadi merged 12 commits intodotnet:mainfrom
CyrusNajmabadi:diagnosticCaching

Conversation

@CyrusNajmabadi
Copy link
Contributor

No description provided.

@ghost ghost added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Feb 6, 2025
using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithMockDiagnosticUpdateSourceRegistrationService, openDocuments: true);

var diagnosticService = Assert.IsType<DiagnosticAnalyzerService>(workspace.GetService<IDiagnosticAnalyzerService>());
var diagnosticService = workspace.GetService<IDiagnosticAnalyzerService>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed all bizarre casting in the tests. tests should validate the actual interface to the rest of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also, we truly only have one impl of this. so all this type testing is really bizarre.

var globalOptions = exportProvider.GetExportedValue<IGlobalOptionService>();

var diagnostics = await analyzer.GetDiagnosticsForIdsAsync(
var diagnostics = await service.GetDiagnosticsForIdsAsync(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed bizarre testing of internal implementation details. Tests should go through the service, not bypass 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.

in a followup, i will make DiagnosticIncrementalAnalyzer private and a nested type inside DIagnosticAnalyzerService.

/// </summary>
private readonly ConcurrentSet<ProjectId> _analyzedProjectIds = [];
private readonly ConcurrentDictionary<ProjectId, ImmutableArray<DiagnosticData>> _analyzedProjectToDiagnostics = [];

Copy link
Contributor Author

Choose a reason for hiding this comment

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

much simpler model. instead of calling onto the service to compute diagnostics and then calling back in to fetch whatever it had cached when we asked it to compute, we just grab and cache the values ourselves whne we ask it to compute. THus not needing any sort of coordination.

/// complete set of non-local document diagnostics.
/// </remarks>
Task<ImmutableArray<DiagnosticData>> GetCachedDiagnosticsAsync(
Workspace workspace, ProjectId projectId, DocumentId? documentId, CancellationToken cancellationToken);
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 whole API can go away from IDiagAnalyzerService now, as it was only used in one place that now owns caching itself.

/// Force analyzes the given project by running all applicable analyzers on the project and caching the reported analyzer diagnostics.
/// </summary>
Task ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken);
Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: the internal api for this already returned these values (which is what unit tests were validating to begin with). This removes any sort of impedence mismatch where tests were validating one thing, but the product code could potentially experience something else because it went through a different untested api.

{
var analyzer = CreateIncrementalAnalyzer(workspace);
return analyzer.GetCachedDiagnosticsAsync(workspace.CurrentSolution, projectId, documentId, cancellationToken);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yaay gone.

internal partial class DiagnosticIncrementalAnalyzer
{
public Task<ImmutableArray<DiagnosticData>> GetCachedDiagnosticsAsync(Solution solution, ProjectId projectId, DocumentId? documentId, CancellationToken cancellationToken)
=> new IdeCachedDiagnosticGetter(this, solution, projectId, documentId).GetDiagnosticsAsync(cancellationToken);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yaay gone.

var nonLocalResult = await state.GetProjectAnalysisDataAsync(project, cancellationToken: cancellationToken).ConfigureAwait(false);
return nonLocalResult.GetOtherDiagnostics();
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yaay, a massive amount of complexity just gone.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review February 6, 2025 23:30
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner February 6, 2025 23:30
@CyrusNajmabadi
Copy link
Contributor Author

@dibarbet @ToddGrun ptal.

@CyrusNajmabadi CyrusNajmabadi merged commit bd6ab1f into dotnet:main Feb 7, 2025
25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the diagnosticCaching branch February 7, 2025 17:31
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Feb 7, 2025
/// and <see cref="GetLastComputedProjectDiagnostics"/>.
/// </summary>
private readonly ConcurrentSet<ProjectId> _analyzedProjectIds = [];
private readonly ConcurrentDictionary<ProjectId, ImmutableArray<DiagnosticData>> _analyzedProjectToDiagnostics = [];
Copy link
Member

Choose a reason for hiding this comment

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

couple of questions

  1. it seems a little weird to me that the diagnostic data is associated with a ProjectId instead of a Project. If the snapshot is different, is the DiagnosticData valid to be re-used? I would assume spans could be all over the place. I can't quite tell, but the previous code did seem to be somewhat associated with a Project?
  2. related to 1), how concerned are we that this cache could potentially go backwards? e.g. diagnostics for version1 of the project return slower, and overwrite diagnostics calculated for version2?
  3. related to 2), is it possible that different callers end up caching different sets of diagnostics? e.g. one caller asks for diagnostics without non-local diagnostics (or w.e)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

taking offline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Answer for 1:

So, remember this is 'Run Code Analysis'. Which a user kicks off manually,. The analysis runs on the 'current solution snapshot' at the time, and caches the results indefinitely (till the next time it is run), or the next time 'Build' is run by the user.

In this case, we want to stores the computed diagnostics around indefinitely, allowing the LSP pull diags system to see that 'old computed snapshot' of the project.

we do not want to store using Project as the key for two reasons:

  1. it will root an old project instance a long time.
  2. the LSP pull diags is asking for the old computed diags for the current project it has.

so ProjectId is the appropriate key for it.

--

the answers to the rest of the questions follow from this.

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.

4 participants