-
Notifications
You must be signed in to change notification settings - Fork 240
PSSA scheduling change #2266
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
base: main
Are you sure you want to change the base?
PSSA scheduling change #2266
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -94,8 +94,6 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) | |||||
|
|
||||||
| private Lazy<PssaCmdletAnalysisEngine> _analysisEngineLazy; | ||||||
|
|
||||||
| private CancellationTokenSource _diagnosticsCancellationTokenSource; | ||||||
|
|
||||||
| private readonly string _pssaModulePath; | ||||||
|
|
||||||
| private string _pssaSettingsFilePath; | ||||||
|
|
@@ -135,37 +133,32 @@ public void StartScriptDiagnostics(ScriptFile[] filesToAnalyze) | |||||
|
|
||||||
| EnsureEngineSettingsCurrent(); | ||||||
|
|
||||||
| // If there's an existing task, we want to cancel it here; | ||||||
| CancellationTokenSource cancellationSource = new(); | ||||||
| CancellationTokenSource oldTaskCancellation = Interlocked.Exchange(ref _diagnosticsCancellationTokenSource, cancellationSource); | ||||||
| if (oldTaskCancellation is not null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| oldTaskCancellation.Cancel(); | ||||||
| oldTaskCancellation.Dispose(); | ||||||
| } | ||||||
| catch (Exception e) | ||||||
| { | ||||||
| _logger.LogError(e, "Exception occurred while cancelling analysis task"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (filesToAnalyze.Length == 0) | ||||||
| { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| Task analysisTask = Task.Run(() => DelayThenInvokeDiagnosticsAsync(filesToAnalyze, _diagnosticsCancellationTokenSource.Token), _diagnosticsCancellationTokenSource.Token); | ||||||
|
|
||||||
| // Ensure that any next corrections request will wait for this diagnostics publication | ||||||
| // Analyze each file independently with its own cancellation token | ||||||
| foreach (ScriptFile file in filesToAnalyze) | ||||||
| { | ||||||
| CorrectionTableEntry fileCorrectionsEntry = _mostRecentCorrectionsByFile.GetOrAdd( | ||||||
| file, | ||||||
| CorrectionTableEntry.CreateForFile); | ||||||
| CorrectionTableEntry fileAnalysisEntry = _mostRecentCorrectionsByFile.GetOrAdd(file, CorrectionTableEntry.CreateForFile); | ||||||
|
|
||||||
| fileCorrectionsEntry.DiagnosticPublish = analysisTask; | ||||||
| CancellationTokenSource cancellationSource = new(); | ||||||
| CancellationTokenSource oldTaskCancellation = Interlocked.Exchange(ref fileAnalysisEntry.CancellationSource, cancellationSource); | ||||||
| if (oldTaskCancellation is not null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| oldTaskCancellation.Cancel(); | ||||||
| oldTaskCancellation.Dispose(); | ||||||
| } | ||||||
| catch (Exception e) | ||||||
| { | ||||||
| _logger.LogError(e, "Exception occurred while cancelling analysis task"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| _ = Task.Run(() => DelayThenInvokeDiagnosticsAsync(file, fileAnalysisEntry), cancellationSource.Token); | ||||||
| } | ||||||
|
Comment on lines
+146
to
162
|
||||||
| } | ||||||
|
|
||||||
|
|
@@ -222,7 +215,9 @@ public async Task<IReadOnlyDictionary<string, IEnumerable<MarkerCorrection>>> Ge | |||||
| } | ||||||
|
|
||||||
| // Wait for diagnostics to be published for this file | ||||||
| #pragma warning disable VSTHRD003 | ||||||
| await corrections.DiagnosticPublish.ConfigureAwait(false); | ||||||
| #pragma warning restore VSTHRD003 | ||||||
|
|
||||||
| return corrections.Corrections; | ||||||
| } | ||||||
|
|
@@ -345,18 +340,20 @@ private void ClearOpenFileMarkers() | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| internal async Task DelayThenInvokeDiagnosticsAsync(ScriptFile[] filesToAnalyze, CancellationToken cancellationToken) | ||||||
| internal async Task DelayThenInvokeDiagnosticsAsync(ScriptFile fileToAnalyze, CorrectionTableEntry fileAnalysisEntry) | ||||||
| { | ||||||
| if (cancellationToken.IsCancellationRequested) | ||||||
| { | ||||||
| return; | ||||||
| } | ||||||
| CancellationToken cancellationToken = fileAnalysisEntry.CancellationSource.Token; | ||||||
| Task previousAnalysisTask = fileAnalysisEntry.DiagnosticPublish; | ||||||
|
|
||||||
| try | ||||||
| { | ||||||
| await Task.Delay(_analysisDelayMillis, cancellationToken).ConfigureAwait(false); | ||||||
| } | ||||||
| catch (TaskCanceledException) | ||||||
| // Shouldn't start a new analysis task until: | ||||||
| // 1. Delay/debounce period finishes (i.e. user has not started typing again) | ||||||
| // 2. Previous analysis task finishes (runspace pool is capped at 1, so we'd be sitting in a queue there) | ||||||
| Task debounceAndPrevious = Task.WhenAll(Task.Delay(_analysisDelayMillis), previousAnalysisTask); | ||||||
|
|
||||||
| // In parallel, we will keep an eye on our cancellation token | ||||||
| Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken); | ||||||
|
|
||||||
| if (cancellationTask == await Task.WhenAny(debounceAndPrevious, cancellationTask).ConfigureAwait(false)) | ||||||
| { | ||||||
| return; | ||||||
| } | ||||||
|
|
@@ -368,16 +365,36 @@ internal async Task DelayThenInvokeDiagnosticsAsync(ScriptFile[] filesToAnalyze, | |||||
| // on. It makes sense to send back the results from the first | ||||||
| // delay period while the second one is ticking away. | ||||||
|
|
||||||
| foreach (ScriptFile scriptFile in filesToAnalyze) | ||||||
| TaskCompletionSource<ScriptFileMarker[]> placeholder = new TaskCompletionSource<ScriptFileMarker[]>(); | ||||||
|
||||||
| TaskCompletionSource<ScriptFileMarker[]> placeholder = new TaskCompletionSource<ScriptFileMarker[]>(); | |
| TaskCompletionSource<ScriptFileMarker[]> placeholder = new TaskCompletionSource<ScriptFileMarker[]>(TaskCreationOptions.RunContinuationsAsynchronously); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does #1838 apply here?
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field 'DiagnosticPublish' can be 'readonly'.
| public Task DiagnosticPublish; | |
| public readonly Task DiagnosticPublish; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DiagnosticPublish gets modified by Interlocked.CompareExchange on line 371 of AnalysisService.cs
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field 'CancellationSource' can be 'readonly'.
| public CancellationTokenSource CancellationSource; | |
| public readonly CancellationTokenSource CancellationSource; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CancellationSource gets modified by Interlocked.Exchange on line 147 of AnalysisService.cs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition where the CancellationTokenSource could be disposed (line 153) while it's still being used to create the Task.Run on line 161. Consider holding a local reference to the cancellation token before potentially disposing the source, or ensure the token is extracted before disposal.