@@ -94,8 +94,6 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)
9494
9595 private Lazy < PssaCmdletAnalysisEngine > _analysisEngineLazy ;
9696
97- private CancellationTokenSource _diagnosticsCancellationTokenSource ;
98-
9997 private readonly string _pssaModulePath ;
10098
10199 private string _pssaSettingsFilePath ;
@@ -135,37 +133,32 @@ public void StartScriptDiagnostics(ScriptFile[] filesToAnalyze)
135133
136134 EnsureEngineSettingsCurrent ( ) ;
137135
138- // If there's an existing task, we want to cancel it here;
139- CancellationTokenSource cancellationSource = new ( ) ;
140- CancellationTokenSource oldTaskCancellation = Interlocked . Exchange ( ref _diagnosticsCancellationTokenSource , cancellationSource ) ;
141- if ( oldTaskCancellation is not null )
142- {
143- try
144- {
145- oldTaskCancellation . Cancel ( ) ;
146- oldTaskCancellation . Dispose ( ) ;
147- }
148- catch ( Exception e )
149- {
150- _logger . LogError ( e , "Exception occurred while cancelling analysis task" ) ;
151- }
152- }
153-
154136 if ( filesToAnalyze . Length == 0 )
155137 {
156138 return ;
157139 }
158140
159- Task analysisTask = Task . Run ( ( ) => DelayThenInvokeDiagnosticsAsync ( filesToAnalyze , _diagnosticsCancellationTokenSource . Token ) , _diagnosticsCancellationTokenSource . Token ) ;
160-
161- // Ensure that any next corrections request will wait for this diagnostics publication
141+ // Analyze each file independently with its own cancellation token
162142 foreach ( ScriptFile file in filesToAnalyze )
163143 {
164- CorrectionTableEntry fileCorrectionsEntry = _mostRecentCorrectionsByFile . GetOrAdd (
165- file ,
166- CorrectionTableEntry . CreateForFile ) ;
144+ CorrectionTableEntry fileAnalysisEntry = _mostRecentCorrectionsByFile . GetOrAdd ( file , CorrectionTableEntry . CreateForFile ) ;
167145
168- fileCorrectionsEntry . DiagnosticPublish = analysisTask ;
146+ CancellationTokenSource cancellationSource = new ( ) ;
147+ CancellationTokenSource oldTaskCancellation = Interlocked . Exchange ( ref fileAnalysisEntry . CancellationSource , cancellationSource ) ;
148+ if ( oldTaskCancellation is not null )
149+ {
150+ try
151+ {
152+ oldTaskCancellation . Cancel ( ) ;
153+ oldTaskCancellation . Dispose ( ) ;
154+ }
155+ catch ( Exception e )
156+ {
157+ _logger . LogError ( e , "Exception occurred while cancelling analysis task" ) ;
158+ }
159+ }
160+
161+ _ = Task . Run ( ( ) => DelayThenInvokeDiagnosticsAsync ( file , fileAnalysisEntry ) , cancellationSource . Token ) ;
169162 }
170163 }
171164
@@ -222,7 +215,9 @@ public async Task<IReadOnlyDictionary<string, IEnumerable<MarkerCorrection>>> Ge
222215 }
223216
224217 // Wait for diagnostics to be published for this file
218+ #pragma warning disable VSTHRD003
225219 await corrections . DiagnosticPublish . ConfigureAwait ( false ) ;
220+ #pragma warning restore VSTHRD003
226221
227222 return corrections . Corrections ;
228223 }
@@ -345,18 +340,20 @@ private void ClearOpenFileMarkers()
345340 }
346341 }
347342
348- internal async Task DelayThenInvokeDiagnosticsAsync ( ScriptFile [ ] filesToAnalyze , CancellationToken cancellationToken )
343+ internal async Task DelayThenInvokeDiagnosticsAsync ( ScriptFile fileToAnalyze , CorrectionTableEntry fileAnalysisEntry )
349344 {
350- if ( cancellationToken . IsCancellationRequested )
351- {
352- return ;
353- }
345+ CancellationToken cancellationToken = fileAnalysisEntry . CancellationSource . Token ;
346+ Task previousAnalysisTask = fileAnalysisEntry . DiagnosticPublish ;
354347
355- try
356- {
357- await Task . Delay ( _analysisDelayMillis , cancellationToken ) . ConfigureAwait ( false ) ;
358- }
359- catch ( TaskCanceledException )
348+ // Shouldn't start a new analysis task until:
349+ // 1. Delay/debounce period finishes (i.e. user has not started typing again)
350+ // 2. Previous analysis task finishes (runspace pool is capped at 1, so we'd be sitting in a queue there)
351+ Task debounceAndPrevious = Task . WhenAll ( Task . Delay ( _analysisDelayMillis ) , previousAnalysisTask ?? Task . CompletedTask ) ;
352+
353+ // In parallel, we will keep an eye on our cancellation token
354+ Task cancellationTask = Task . Delay ( Timeout . Infinite , cancellationToken ) ;
355+
356+ if ( cancellationTask == await Task . WhenAny ( debounceAndPrevious , cancellationTask ) . ConfigureAwait ( false ) )
360357 {
361358 return ;
362359 }
@@ -368,16 +365,35 @@ internal async Task DelayThenInvokeDiagnosticsAsync(ScriptFile[] filesToAnalyze,
368365 // on. It makes sense to send back the results from the first
369366 // delay period while the second one is ticking away.
370367
371- foreach ( ScriptFile scriptFile in filesToAnalyze )
368+ TaskCompletionSource < ScriptFileMarker [ ] > placeholder = new TaskCompletionSource < ScriptFileMarker [ ] > ( ) ;
369+
370+ // Try to take the place of the currently running task by atomically writing our task in the fileAnalysisEntry.
371+ Task valueAtExchange = Interlocked . CompareExchange ( ref fileAnalysisEntry . DiagnosticPublish , placeholder . Task , previousAnalysisTask ) ;
372+
373+ if ( valueAtExchange != previousAnalysisTask ) {
374+ // Some other task has managed to jump in front of us i.e. fileAnalysisEntry.DiagnosticPublish is
375+ // no longer equal to previousAnalysisTask which we noted down at the start of this method
376+ _logger . LogDebug ( "Failed to claim the running analysis task spot" ) ;
377+ return ;
378+ }
379+
380+ // Successfully claimed the running task slot, we can actually run the analysis now
381+ try
372382 {
373- ScriptFileMarker [ ] semanticMarkers = await AnalysisEngine . AnalyzeScriptAsync ( scriptFile . Contents ) . ConfigureAwait ( false ) ;
383+ ScriptFileMarker [ ] semanticMarkers = await AnalysisEngine . AnalyzeScriptAsync ( fileToAnalyze . Contents ) . ConfigureAwait ( false ) ;
384+ placeholder . SetResult ( semanticMarkers ) ;
374385
375386 // Clear existing PSScriptAnalyzer markers (but keep parser errors where the source is "PowerShell")
376387 // so that they are not duplicated when re-opening files.
377- scriptFile . DiagnosticMarkers . RemoveAll ( m => m . Source == "PSScriptAnalyzer" ) ;
378- scriptFile . DiagnosticMarkers . AddRange ( semanticMarkers ) ;
388+ fileToAnalyze . DiagnosticMarkers . RemoveAll ( m => m . Source == "PSScriptAnalyzer" ) ;
389+ fileToAnalyze . DiagnosticMarkers . AddRange ( semanticMarkers ) ;
379390
380- PublishScriptDiagnostics ( scriptFile ) ;
391+ PublishScriptDiagnostics ( fileToAnalyze ) ;
392+ }
393+ catch ( Exception ex )
394+ {
395+ placeholder . SetException ( ex ) ;
396+ throw ;
381397 }
382398 }
383399
@@ -480,8 +496,6 @@ protected virtual void Dispose(bool disposing)
480496 {
481497 _analysisEngineLazy . Value . Dispose ( ) ;
482498 }
483-
484- _diagnosticsCancellationTokenSource ? . Dispose ( ) ;
485499 }
486500
487501 disposedValue = true ;
@@ -501,19 +515,25 @@ public void Dispose() =>
501515 /// wait for analysis to finish if needed,
502516 /// and then fetch the corrections set in the table entry by PSSA.
503517 /// </summary>
504- private class CorrectionTableEntry
518+ internal class CorrectionTableEntry : IDisposable
505519 {
506520 public static CorrectionTableEntry CreateForFile ( ScriptFile _ ) => new ( ) ;
507521
508522 public CorrectionTableEntry ( )
509523 {
510524 Corrections = new ConcurrentDictionary < string , IEnumerable < MarkerCorrection > > ( ) ;
511525 DiagnosticPublish = Task . CompletedTask ;
526+ CancellationSource = new CancellationTokenSource ( ) ;
512527 }
513528
514529 public ConcurrentDictionary < string , IEnumerable < MarkerCorrection > > Corrections { get ; }
515530
516- public Task DiagnosticPublish { get ; set ; }
531+ public Task DiagnosticPublish ;
532+
533+ public CancellationTokenSource CancellationSource ;
534+
535+ public void Dispose ( ) =>
536+ CancellationSource ? . Dispose ( ) ;
517537 }
518538 }
519539}
0 commit comments