diff --git a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md index 6544ab4685f..7d8270224ac 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -13,4 +13,5 @@ * `implicitCtorSynPats` in `SynTypeDefnSimpleRepr.General` is now `SynPat option` instead of `SynSimplePats option`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) * `SyntaxVisitorBase<'T>.VisitSimplePats` now takes `SynPat` instead of `SynSimplePat list`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) -* Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323)) \ No newline at end of file +* Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323)) +* Rework cancellation in `AsyncMemoize`. ([PR #16547](https://github.com/dotnet/fsharp/pull/16547)) \ No newline at end of file diff --git a/src/Compiler/Facilities/AsyncMemoize.fs b/src/Compiler/Facilities/AsyncMemoize.fs index ad798140cb9..c3b8a259c97 100644 --- a/src/Compiler/Facilities/AsyncMemoize.fs +++ b/src/Compiler/Facilities/AsyncMemoize.fs @@ -7,7 +7,6 @@ open System.IO open System.Threading open System.Threading.Tasks -open FSharp.Compiler open FSharp.Compiler.BuildGraph open FSharp.Compiler.Diagnostics open FSharp.Compiler.DiagnosticsLogger @@ -113,14 +112,24 @@ type internal AsyncLock() = member _.Semaphore = semaphore - member _.Do(f) = + member _.Gate() = task { do! semaphore.WaitAsync() - try - return! f () - finally - semaphore.Release() |> ignore + return + { new IDisposable with + member _.Dispose() = + try + semaphore.Release() |> ignore + with :? ObjectDisposedException -> + () + } + } + + member this.Do(f) = + task { + use! _gate = this.Gate() + return! f () } interface IDisposable with @@ -164,10 +173,8 @@ type internal AsyncMemoize<'TKey, 'TVersion, 'TValue when 'TKey: equality and 'T let mutable cleared = 0 let mutable cancel_ct_registration_original = 0 - let mutable cancel_exception_original = 0 let mutable cancel_original_processed = 0 let mutable cancel_ct_registration_subsequent = 0 - let mutable cancel_exception_subsequent = 0 let mutable cancel_subsequent_processed = 0 let failures = ResizeArray() @@ -238,224 +245,196 @@ type internal AsyncMemoize<'TKey, 'TVersion, 'TValue when 'TKey: equality and 'T let lock = new AsyncLock() - let processRequest post (key: KeyData<_, _>, msg) diagnosticLogger = - - lock.Do(fun () -> - task { - - let cached, otherVersions = cache.GetAll(key.Key, key.Version) - - return - match msg, cached with - | GetOrCompute _, Some(Completed(result, diags)) -> - Interlocked.Increment &hits |> ignore - diags |> replayDiagnostics diagnosticLogger - Existing(Task.FromResult result) - | GetOrCompute(_, ct), Some(Running(tcs, _, _, _, loggers)) -> - Interlocked.Increment &hits |> ignore - incrRequestCount key - - ct.Register(fun _ -> - let _name = name - Interlocked.Increment &cancel_ct_registration_subsequent |> ignore - post (key, CancelRequest)) - |> saveRegistration key - - loggers.Add diagnosticLogger - - Existing tcs.Task - - | GetOrCompute(computation, ct), None - | GetOrCompute(computation, ct), Some(Job.Canceled _) - | GetOrCompute(computation, ct), Some(Job.Failed _) -> - Interlocked.Increment &started |> ignore - incrRequestCount key - - ct.Register(fun _ -> - let _name = name - Interlocked.Increment &cancel_ct_registration_original |> ignore - post (key, OriginatorCanceled)) - |> saveRegistration key - - let cts = new CancellationTokenSource() - - cache.Set( - key.Key, - key.Version, - key.Label, - (Running(TaskCompletionSource(), cts, computation, DateTime.Now, ResizeArray())) - ) - - otherVersions - |> Seq.choose (function - | v, Running(_tcs, cts, _, _, _) -> Some(v, cts) - | _ -> None) - |> Seq.iter (fun (_v, cts) -> - use _ = Activity.start $"{name}: Duplicate running job" [| "key", key.Label |] - //System.Diagnostics.Trace.TraceWarning($"{name} Duplicate {key.Label}") - if cancelDuplicateRunningJobs then - //System.Diagnostics.Trace.TraceWarning("Canceling") - cts.Cancel()) - - New cts.Token - }) - let internalError key message = let ex = exn (message) failures.Add(key, ex) Interlocked.Increment &errors |> ignore // raise ex -- Suppose there's no need to raise here - where does it even go? - let processStateUpdate post (key: KeyData<_, _>, action: StateUpdate<_>) = + let compute computation ct = + Async.StartImmediateAsTask(Async.AwaitNodeCode computation, cancellationToken = ct) + + let rec post msg = processStateUpdate msg |> ignore + + and start key computation ct logger = + task { + let cachingLogger = new CachingDiagnosticsLogger(Some logger) + + try + let currentLogger = DiagnosticsThreadStatics.DiagnosticsLogger + DiagnosticsThreadStatics.DiagnosticsLogger <- cachingLogger + log (Started, key) + + try + let! result = compute computation ct + post (key, (JobCompleted(result, cachingLogger.CapturedDiagnostics))) + return result + finally + DiagnosticsThreadStatics.DiagnosticsLogger <- currentLogger + with + | TaskCancelled ex -> return raise ex + | ex -> + post (key, (JobFailed(ex, cachingLogger.CapturedDiagnostics))) + return raise ex + } + + and cancel key (cts: CancellationTokenSource) (tcs: TaskCompletionSource<_>) = + cancelRegistration key + cts.Cancel() + tcs.TrySetCanceled() |> ignore + // Remember the job in case it completes after cancellation + cache.Set(key.Key, key.Version, key.Label, Job.Canceled DateTime.Now) + requestCounts.Remove key |> ignore + log (Canceled, key) + Interlocked.Increment &canceled |> ignore + use _ = Activity.start $"{name}: Canceled job" [| "key", key.Label |] + () + + and processStateUpdate (key: KeyData<_, _>, action: StateUpdate<_>) = task { - do! Task.Delay 0 - - do! - lock.Do(fun () -> - task { - - let cached = cache.TryGet(key.Key, key.Version) - - match action, cached with - - | OriginatorCanceled, Some(Running(tcs, cts, computation, _, _)) -> - - Interlocked.Increment &cancel_original_processed |> ignore - - decrRequestCount key - - if requestCounts[key] < 1 then - cancelRegistration key - cts.Cancel() - tcs.TrySetCanceled() |> ignore - // Remember the job in case it completes after cancellation - cache.Set(key.Key, key.Version, key.Label, Job.Canceled DateTime.Now) - requestCounts.Remove key |> ignore - log (Canceled, key) - Interlocked.Increment &canceled |> ignore - use _ = Activity.start $"{name}: Canceled job" [| "key", key.Label |] - () - - else - // We need to restart the computation - Task.Run(fun () -> - Async.StartAsTask( - async { - - let cachingLogger = new CachingDiagnosticsLogger(None) - - try - // TODO: Should unify starting and restarting - use _ = Cancellable.UsingToken(cts.Token) - log (Started, key) - Interlocked.Increment &restarted |> ignore - System.Diagnostics.Trace.TraceInformation $"{name} Restarted {key.Label}" - let currentLogger = DiagnosticsThreadStatics.DiagnosticsLogger - DiagnosticsThreadStatics.DiagnosticsLogger <- cachingLogger - - try - let! result = computation |> Async.AwaitNodeCode - post (key, (JobCompleted(result, cachingLogger.CapturedDiagnostics))) - return () - finally - DiagnosticsThreadStatics.DiagnosticsLogger <- currentLogger - with - | TaskCancelled _ -> - Interlocked.Increment &cancel_exception_subsequent |> ignore - post (key, CancelRequest) - () - | ex -> post (key, (JobFailed(ex, cachingLogger.CapturedDiagnostics))) - } - ), - cts.Token) - |> ignore - - | CancelRequest, Some(Running(tcs, cts, _c, _, _)) -> - - Interlocked.Increment &cancel_subsequent_processed |> ignore - - decrRequestCount key - - if requestCounts[key] < 1 then - cancelRegistration key - cts.Cancel() - tcs.TrySetCanceled() |> ignore - // Remember the job in case it completes after cancellation - cache.Set(key.Key, key.Version, key.Label, Job.Canceled DateTime.Now) - requestCounts.Remove key |> ignore - log (Canceled, key) - Interlocked.Increment &canceled |> ignore - use _ = Activity.start $"{name}: Canceled job" [| "key", key.Label |] - () - - // Probably in some cases cancellation can be fired off even after we just unregistered it - | CancelRequest, None - | CancelRequest, Some(Completed _) - | CancelRequest, Some(Job.Canceled _) - | CancelRequest, Some(Job.Failed _) - | OriginatorCanceled, None - | OriginatorCanceled, Some(Completed _) - | OriginatorCanceled, Some(Job.Canceled _) - | OriginatorCanceled, Some(Job.Failed _) -> () - - | JobFailed(ex, diags), Some(Running(tcs, _cts, _c, _ts, loggers)) -> - cancelRegistration key - cache.Set(key.Key, key.Version, key.Label, Job.Failed(DateTime.Now, ex)) - requestCounts.Remove key |> ignore - log (Failed, key) - Interlocked.Increment &failed |> ignore - failures.Add(key.Label, ex) - - for logger in loggers do - diags |> replayDiagnostics logger - - tcs.TrySetException ex |> ignore - - | JobCompleted(result, diags), Some(Running(tcs, _cts, _c, started, loggers)) -> - cancelRegistration key - cache.Set(key.Key, key.Version, key.Label, (Completed(result, diags))) - requestCounts.Remove key |> ignore - log (Finished, key) - Interlocked.Increment &completed |> ignore - let duration = float (DateTime.Now - started).Milliseconds - - avgDurationMs <- - if completed < 2 then - duration - else - avgDurationMs + (duration - avgDurationMs) / float completed - - for logger in loggers do - diags |> replayDiagnostics logger - - if tcs.TrySetResult result = false then - internalError key.Label "Invalid state: Completed job already completed" - - // Sometimes job can be canceled but it still manages to complete (or fail) - | JobFailed _, Some(Job.Canceled _) - | JobCompleted _, Some(Job.Canceled _) -> () - - // Job can't be evicted from cache while it's running because then subsequent requesters would be waiting forever - | JobFailed _, None -> internalError key.Label "Invalid state: Running job missing in cache (failed)" - - | JobCompleted _, None -> internalError key.Label "Invalid state: Running job missing in cache (completed)" - - | JobFailed(ex, _diags), Some(Completed(_job, _diags2)) -> - internalError key.Label $"Invalid state: Failed Completed job \n%A{ex}" - - | JobCompleted(_result, _diags), Some(Completed(_job, _diags2)) -> - internalError key.Label "Invalid state: Double-Completed job" - - | JobFailed(ex, _diags), Some(Job.Failed(_, ex2)) -> - internalError key.Label $"Invalid state: Double-Failed job \n%A{ex} \n%A{ex2}" - - | JobCompleted(_result, _diags), Some(Job.Failed(_, ex2)) -> - internalError key.Label $"Invalid state: Completed Failed job \n%A{ex2}" - }) + do! Task.Yield() + use! _gate = lock.Gate() + + let cached = cache.TryGet(key.Key, key.Version) + + match action, cached with + + | OriginatorCanceled, Some(Running(tcs, cts, _, _, _)) -> + + Interlocked.Increment &cancel_original_processed |> ignore + + decrRequestCount key + + if requestCounts[key] < 1 then + cancel key cts tcs + + | CancelRequest, Some(Running(tcs, cts, _c, _, _)) -> + + Interlocked.Increment &cancel_subsequent_processed |> ignore + + decrRequestCount key + + if requestCounts[key] < 1 then + cancel key cts tcs + + // Probably in some cases cancellation can be fired off even after we just unregistered it + | CancelRequest, None + | CancelRequest, Some(Completed _) + | CancelRequest, Some(Job.Canceled _) + | CancelRequest, Some(Job.Failed _) + | OriginatorCanceled, None + | OriginatorCanceled, Some(Completed _) + | OriginatorCanceled, Some(Job.Canceled _) + | OriginatorCanceled, Some(Job.Failed _) -> () + + | JobFailed(ex, diags), Some(Running(tcs, _cts, _c, _ts, loggers)) -> + cancelRegistration key + cache.Set(key.Key, key.Version, key.Label, Job.Failed(DateTime.Now, ex)) + requestCounts.Remove key |> ignore + log (Failed, key) + Interlocked.Increment &failed |> ignore + failures.Add(key.Label, ex) + + for logger in loggers do + diags |> replayDiagnostics logger + + tcs.TrySetException ex |> ignore + + | JobCompleted(result, diags), Some(Running(tcs, _cts, _c, started, loggers)) -> + cancelRegistration key + cache.Set(key.Key, key.Version, key.Label, (Completed(result, diags))) + requestCounts.Remove key |> ignore + log (Finished, key) + Interlocked.Increment &completed |> ignore + let duration = float (DateTime.Now - started).Milliseconds + + avgDurationMs <- + if completed < 2 then + duration + else + avgDurationMs + (duration - avgDurationMs) / float completed + + for logger in loggers do + diags |> replayDiagnostics logger + + if tcs.TrySetResult result = false then + internalError key.Label "Invalid state: Completed job already completed" + + // Sometimes job can be canceled but it still manages to complete (or fail) + | JobFailed _, Some(Job.Canceled _) + | JobCompleted _, Some(Job.Canceled _) -> () + + // Job can't be evicted from cache while it's running because then subsequent requesters would be waiting forever + | JobFailed _, None -> internalError key.Label "Invalid state: Running job missing in cache (failed)" + + | JobCompleted _, None -> internalError key.Label "Invalid state: Running job missing in cache (completed)" + + | JobFailed(ex, _diags), Some(Completed(_job, _diags2)) -> + internalError key.Label $"Invalid state: Failed Completed job \n%A{ex}" + + | JobCompleted(_result, _diags), Some(Completed(_job, _diags2)) -> internalError key.Label "Invalid state: Double-Completed job" + + | JobFailed(ex, _diags), Some(Job.Failed(_, ex2)) -> + internalError key.Label $"Invalid state: Double-Failed job \n%A{ex} \n%A{ex2}" + + | JobCompleted(_result, _diags), Some(Job.Failed(_, ex2)) -> + internalError key.Label $"Invalid state: Completed Failed job \n%A{ex2}" } - let rec post msg = - Task.Run(fun () -> processStateUpdate post msg :> Task) |> ignore + let processRequest (key: KeyData<_, _>, msg) diagnosticLogger = + task { + use! _gate = lock.Gate() + + let cached, otherVersions = cache.GetAll(key.Key, key.Version) + + match msg, cached with + | GetOrCompute _, Some(Completed(result, diags)) -> + Interlocked.Increment &hits |> ignore + diags |> replayDiagnostics diagnosticLogger + return Existing(Task.FromResult result) + | GetOrCompute(_, ct), Some(Running(tcs, _, _, _, loggers)) -> + Interlocked.Increment &hits |> ignore + incrRequestCount key + + ct.Register(fun _ -> + let _name = name + Interlocked.Increment &cancel_ct_registration_subsequent |> ignore + post (key, CancelRequest)) + |> saveRegistration key + + loggers.Add diagnosticLogger + + return Existing tcs.Task + + | GetOrCompute(computation, ct), None + | GetOrCompute(computation, ct), Some(Job.Canceled _) + | GetOrCompute(computation, ct), Some(Job.Failed _) -> + Interlocked.Increment &started |> ignore + incrRequestCount key + + ct.Register(fun _ -> + let _name = name + Interlocked.Increment &cancel_ct_registration_original |> ignore + post (key, OriginatorCanceled)) + |> saveRegistration key + + let cts = new CancellationTokenSource() + + cache.Set(key.Key, key.Version, key.Label, (Running(TaskCompletionSource(), cts, computation, DateTime.Now, ResizeArray()))) + + otherVersions + |> Seq.choose (function + | v, Running(_tcs, cts, _, _, _) -> Some(v, cts) + | _ -> None) + |> Seq.iter (fun (_v, cts) -> + use _ = Activity.start $"{name}: Duplicate running job" [| "key", key.Label |] + //System.Diagnostics.Trace.TraceWarning($"{name} Duplicate {key.Label}") + if cancelDuplicateRunningJobs then + //System.Diagnostics.Trace.TraceWarning("Canceling") + cts.Cancel()) + + return New cts.Token + } member this.Get'(key, computation) = @@ -477,57 +456,18 @@ type internal AsyncMemoize<'TKey, 'TVersion, 'TValue when 'TKey: equality and 'T Version = key.GetVersion() } - node { - let! ct = NodeCode.CancellationToken - - let callerDiagnosticLogger = DiagnosticsThreadStatics.DiagnosticsLogger - - match! - processRequest post (key, GetOrCompute(computation, ct)) callerDiagnosticLogger - |> NodeCode.AwaitTask - with - | New internalCt -> - - let linkedCtSource = CancellationTokenSource.CreateLinkedTokenSource(ct, internalCt) - let cachingLogger = new CachingDiagnosticsLogger(Some callerDiagnosticLogger) + let getOrCompute ct = + task { + let callerDiagnosticLogger = DiagnosticsThreadStatics.DiagnosticsLogger - try - return! - Async.StartAsTask( - async { - // TODO: Should unify starting and restarting - let currentLogger = DiagnosticsThreadStatics.DiagnosticsLogger - DiagnosticsThreadStatics.DiagnosticsLogger <- cachingLogger - use _ = Cancellable.UsingToken(internalCt) - log (Started, key) - - try - let! result = computation |> Async.AwaitNodeCode - post (key, (JobCompleted(result, cachingLogger.CapturedDiagnostics))) - return result - finally - DiagnosticsThreadStatics.DiagnosticsLogger <- currentLogger - }, - cancellationToken = linkedCtSource.Token - ) - |> NodeCode.AwaitTask - with - | TaskCancelled ex -> - // TODO: do we need to do anything else here? Presumably it should be done by the registration on - // the cancellation token or before we triggered our own cancellation - - // Let's send this again just in case. It seems sometimes it's not triggered from the registration? - - Interlocked.Increment &cancel_exception_original |> ignore - - post (key, (OriginatorCanceled)) - return raise ex - | ex -> - post (key, (JobFailed(ex, cachingLogger.CapturedDiagnostics))) - return raise ex - - | Existing job -> return! job |> NodeCode.AwaitTask + match! processRequest (key, GetOrCompute(computation, ct)) callerDiagnosticLogger with + | New internalCt -> return! start key computation internalCt callerDiagnosticLogger + | Existing job -> return! job + } + node { + let! ct = NodeCode.CancellationToken + return! getOrCompute ct |> Async.AwaitTask |> NodeCode.AwaitAsync } member _.Clear() = cache.Clear() diff --git a/src/Compiler/Facilities/BuildGraph.fs b/src/Compiler/Facilities/BuildGraph.fs index 8927862c23c..062626a32cd 100644 --- a/src/Compiler/Facilities/BuildGraph.fs +++ b/src/Compiler/Facilities/BuildGraph.fs @@ -17,14 +17,12 @@ let wrapThreadStaticInfo computation = async { let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger let phase = DiagnosticsThreadStatics.BuildPhase - let ct = Cancellable.Token try return! computation finally DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct } type Async<'T> with @@ -127,7 +125,6 @@ type NodeCode private () = static member RunImmediate(computation: NodeCode<'T>, ct: CancellationToken) = let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger let phase = DiagnosticsThreadStatics.BuildPhase - let ct2 = Cancellable.Token try try @@ -135,7 +132,6 @@ type NodeCode private () = async { DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 return! computation |> Async.AwaitNodeCode } @@ -143,7 +139,6 @@ type NodeCode private () = finally DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 with :? AggregateException as ex when ex.InnerExceptions.Count = 1 -> raise (ex.InnerExceptions[0]) @@ -151,24 +146,7 @@ type NodeCode private () = NodeCode.RunImmediate(computation, CancellationToken.None) static member StartAsTask_ForTesting(computation: NodeCode<'T>, ?ct: CancellationToken) = - let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger - let phase = DiagnosticsThreadStatics.BuildPhase - let ct2 = Cancellable.Token - - try - let work = - async { - DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger - DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 - return! computation |> Async.AwaitNodeCode - } - - Async.StartAsTask(work, cancellationToken = defaultArg ct CancellationToken.None) - finally - DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger - DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 + Async.StartAsTask(computation |> Async.AwaitNodeCode, ?cancellationToken = ct) static member CancellationToken = cancellationToken diff --git a/src/Compiler/Interactive/fsi.fs b/src/Compiler/Interactive/fsi.fs index ca0e2335064..e5ff5b6c754 100644 --- a/src/Compiler/Interactive/fsi.fs +++ b/src/Compiler/Interactive/fsi.fs @@ -4089,7 +4089,6 @@ type FsiInteractionProcessor ?cancellationToken: CancellationToken ) = let cancellationToken = defaultArg cancellationToken CancellationToken.None - use _ = Cancellable.UsingToken(cancellationToken) if tokenizer.LexBuffer.IsPastEndOfStream then let stepStatus = @@ -4218,7 +4217,6 @@ type FsiInteractionProcessor member _.EvalInteraction(ctok, sourceText, scriptFileName, diagnosticsLogger, ?cancellationToken) = let cancellationToken = defaultArg cancellationToken CancellationToken.None - use _ = Cancellable.UsingToken(cancellationToken) use _ = UseBuildPhase BuildPhase.Interactive use _ = UseDiagnosticsLogger diagnosticsLogger use _scope = SetCurrentUICultureForThread fsiOptions.FsiLCID @@ -4895,7 +4893,6 @@ type FsiEvaluationSession SpawnInteractiveServer(fsi, fsiOptions, fsiConsoleOutput) use _ = UseBuildPhase BuildPhase.Interactive - use _ = Cancellable.UsingToken(CancellationToken.None) if fsiOptions.Interact then // page in the type check env diff --git a/src/Compiler/Service/BackgroundCompiler.fs b/src/Compiler/Service/BackgroundCompiler.fs index 514bb8e45c5..a85a86f209f 100644 --- a/src/Compiler/Service/BackgroundCompiler.fs +++ b/src/Compiler/Service/BackgroundCompiler.fs @@ -574,9 +574,6 @@ type internal BackgroundCompiler Activity.Tags.cache, cache.ToString() |] - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) - if cache then let hash = sourceText.GetHashCode() |> int64 @@ -629,9 +626,6 @@ type internal BackgroundCompiler "BackgroundCompiler.GetBackgroundParseResultsForFileInProject" [| Activity.Tags.fileName, fileName; Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -783,9 +777,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! cachedResults = node { let! builderOpt, creationDiags = getAnyBuilder (options, userOpName) @@ -846,9 +837,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -897,9 +885,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -969,9 +954,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, _ = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -991,9 +973,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -1134,9 +1113,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, _ = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -1185,8 +1161,6 @@ type internal BackgroundCompiler /// Parse and typecheck the whole project (the implementation, called recursively as project graph is evaluated) member private _.ParseAndCheckProjectImpl(options, userOpName) = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) @@ -1327,9 +1301,6 @@ type internal BackgroundCompiler // Do we assume .NET Framework references for scripts? let assumeDotNetFramework = defaultArg assumeDotNetFramework true - let! ct = Cancellable.token () - use _ = Cancellable.UsingToken(ct) - let extraFlags = if previewEnabled then [| "--langversion:preview" |] @@ -1452,8 +1423,6 @@ type internal BackgroundCompiler |] async { - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) let! ct = Async.CancellationToken // If there was a similar entry (as there normally will have been) then re-establish an empty builder . This diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 48b9875c9f0..cdefe667394 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -3585,8 +3585,6 @@ type FsiInteractiveChecker(legacyReferenceResolver, tcConfig: TcConfig, tcGlobal member _.ParseAndCheckInteraction(sourceText: ISourceText, ?userOpName: string) = cancellable { - let! ct = Cancellable.token () - use _ = Cancellable.UsingToken(ct) let userOpName = defaultArg userOpName "Unknown" let fileName = Path.Combine(tcConfig.implicitIncludeDir, "stdin.fsx") diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index fe82627483f..ab64f2f7418 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -1762,8 +1762,6 @@ type internal TransparentCompiler node { //use _ = // Activity.start "ParseFile" [| Activity.Tags.fileName, fileName |> Path.GetFileName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) // TODO: might need to deal with exceptions here: let tcConfigB, sourceFileNames, _ = ComputeTcConfigBuilder projectSnapshot @@ -1792,6 +1790,7 @@ type internal TransparentCompiler ignore userOpName node { + match! ComputeItemKeyStore(fileName, projectSnapshot) with | None -> return Seq.empty | Some itemKeyStore -> return itemKeyStore.FindAll symbol.Item @@ -1818,8 +1817,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) @@ -1842,8 +1839,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) @@ -1897,8 +1892,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode> = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) ignore canInvalidateProject let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync @@ -1914,8 +1907,6 @@ type internal TransparentCompiler member this.GetAssemblyData(options: FSharpProjectOptions, fileName, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! this.GetAssemblyData(snapshot.ProjectSnapshot, fileName, userOpName) @@ -1936,8 +1927,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync @@ -1953,8 +1942,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! this.ParseFile(fileName, snapshot.ProjectSnapshot, userOpName) @@ -1969,8 +1956,6 @@ type internal TransparentCompiler ) : NodeCode<(FSharpParseFileResults * FSharpCheckFileResults) option> = node { ignore builder - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, 1, sourceText) @@ -2023,8 +2008,6 @@ type internal TransparentCompiler ) : NodeCode = node { ignore userOpName - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! ComputeSemanticClassification(fileName, snapshot.ProjectSnapshot) @@ -2048,8 +2031,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) @@ -2063,8 +2044,6 @@ type internal TransparentCompiler member this.ParseAndCheckProject(options: FSharpProjectOptions, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) ignore userOpName let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync @@ -2073,8 +2052,6 @@ type internal TransparentCompiler member this.ParseAndCheckProject(projectSnapshot: FSharpProjectSnapshot, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) ignore userOpName return! ComputeParseAndCheckProject projectSnapshot.ProjectSnapshot diff --git a/src/Compiler/Service/service.fs b/src/Compiler/Service/service.fs index b79254d7935..5ac50733448 100644 --- a/src/Compiler/Service/service.fs +++ b/src/Compiler/Service/service.fs @@ -348,8 +348,6 @@ type FSharpChecker use _ = Activity.start "FSharpChecker.Compile" [| Activity.Tags.userOpName, _userOpName |] async { - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) let ctok = CompilationThreadToken() return CompileHelpers.compileFromArgs (ctok, argv, legacyReferenceResolver, None, None) @@ -485,8 +483,6 @@ type FSharpChecker let userOpName = defaultArg userOpName "Unknown" node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) if fastCheck <> Some true || not captureIdentifiersWhenParsing then return! backgroundCompiler.FindReferencesInFile(fileName, options, symbol, canInvalidateProject, userOpName) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs index 817a3b8c70a..a2dcb8ff2dc 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs @@ -11,27 +11,24 @@ open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.Diagnostics open FSharp.Compiler.BuildGraph -[] -let ``Stack trace`` () = - - let memoize = AsyncMemoize() - - let computation key = node { - // do! Async.Sleep 1 |> NodeCode.AwaitAsync - let! result = memoize.Get'(key * 2, node { - //do! Async.Sleep 1 |> NodeCode.AwaitAsync - return key * 5 - }) +let timeout = TimeSpan.FromSeconds 10 - return result * 2 +let waitForA (mre: EventWaitHandle) = + async { + let! waitResult = Async.AwaitWaitHandle(mre, int timeout.TotalMilliseconds) + if not waitResult then failwith "waitFor timed out" } - //let _r2 = computation 10 - - let result = memoize.Get'(1, computation 1) |> NodeCode.RunImmediateWithoutCancellation +let waitFor h = h |> waitForA |> Async.StartImmediateAsTask +let internal waitForN h = h |> waitForA |> NodeCode.AwaitAsync - Assert.Equal(10, result) +let rec internal spinFor (duration: TimeSpan) = + node { + let sw = Stopwatch.StartNew() + do! Async.Sleep 10 |> NodeCode.AwaitAsync + return! spinFor (duration - sw.Elapsed) + } [] @@ -74,64 +71,54 @@ let ``We can cancel a job`` () = let jobStarted = new ManualResetEvent(false) - let computation key = node { - jobStarted.Set() |> ignore - do! Async.Sleep 1000 |> NodeCode.AwaitAsync + let jobCanceled = new ManualResetEvent(false) + + let computation action = node { + action() |> ignore + do! spinFor timeout failwith "Should be canceled before it gets here" - return key * 2 } - let eventLog = ResizeArray() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (e, k)) + let eventLog = ConcurrentQueue() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_label, k, _version)) -> + eventLog.Enqueue (e, k) + if e = Canceled then + jobCanceled.Set() |> ignore + ) use cts1 = new CancellationTokenSource() - use cts2 = new CancellationTokenSource() - use cts3 = new CancellationTokenSource() + let key = 1 - let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) + let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation jobStarted.Set), ct = cts1.Token) - jobStarted.WaitOne() |> ignore + do! waitFor jobStarted jobStarted.Reset() |> ignore - let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) - let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - - Assert.Equal<(JobEvent * int) array>([| Started, key |], eventLog |> Seq.toArray ) - - do! Task.Delay 100 - cts1.Cancel() - cts2.Cancel() - - do! Task.Delay 100 - cts3.Cancel() - - do! Task.Delay 100 + do! waitFor jobCanceled - Assert.Equal<(JobEvent * int) array>([| Started, key; Started, key; Canceled, key |], eventLog |> Seq.toArray ) + Assert.Equal<(JobEvent * int) array>([| Started, key; Canceled, key |], eventLog |> Seq.toArray ) } [] -let ``Job is restarted if first requestor cancels`` () = +let ``Job is not cancelled if just one requestor cancels`` () = task { let jobStarted = new ManualResetEvent(false) + let jobCanComplete = new ManualResetEvent(false) let computation key = node { jobStarted.Set() |> ignore - - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync - + do! waitForN jobCanComplete return key * 2 } - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) + let eventLog = ConcurrentStack() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_, k, _version)) -> eventLog.Push (e, k)) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -140,46 +127,38 @@ let ``Job is restarted if first requestor cancels`` () = let key = 1 let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) - jobStarted.WaitOne() |> ignore - let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - do! Task.Delay 100 + do! waitFor jobStarted cts1.Cancel() - - do! Task.Delay 100 cts3.Cancel() + jobCanComplete.Set() |> ignore + let! result = _task2 Assert.Equal(2, result) - Assert.Equal(TaskStatus.Canceled, _task1.Status) - - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList - let expected = [ Started, key; Started, key; Finished, key ] + let orderedLog = eventLog |> Seq.rev |> Seq.toList + let expected = [ Started, key; Finished, key ] Assert.Equal<_ list>(expected, orderedLog) } -// [] - if we decide to enable that -let ``Job keeps running if the first requestor cancels`` () = +[] +let ``Job is not cancelled while there are requestors`` () = task { - let jobStarted = new ManualResetEvent(false) + let jobCanComplete = new ManualResetEvent(false) let computation key = node { - jobStarted.Set() |> ignore - - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync - - return key * 2 + do! waitForN jobCanComplete + return key * 2 } - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) + let eventLog = ConcurrentStack() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Push (e, k)) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -187,47 +166,45 @@ let ``Job keeps running if the first requestor cancels`` () = let key = 1 - let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) - jobStarted.WaitOne() |> ignore - + let _task1 = NodeCode.StartAsTask_ForTesting(memoize.Get'(key, computation key), ct = cts1.Token) let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - jobStarted.WaitOne() |> ignore - cts1.Cancel() - do! Task.Delay 100 - cts3.Cancel() + cts2.Cancel() - let! result = _task2 - Assert.Equal(2, result) + jobCanComplete.Set() |> ignore - Assert.Equal(TaskStatus.Canceled, _task1.Status) + let! result = _task3 + Assert.Equal(2, result) - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList + let orderedLog = eventLog |> Seq.rev |> Seq.toList let expected = [ Started, key; Finished, key ] Assert.Equal<_ list>(expected, orderedLog) } [] -let ``Job is restarted if first requestor cancels but keeps running if second requestor cancels`` () = +let ``Job is cancelled when all requestors cancel`` () = task { let jobStarted = new ManualResetEvent(false) - let computation key = node { - jobStarted.Set() |> ignore + let jobCanComplete = new ManualResetEvent(false) - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync + use eventTriggered = new AutoResetEvent(false) - return key * 2 + let computation key = node { + jobStarted.Set() |> ignore + do! waitForN jobCanComplete + return key * 2 } - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) + let eventLog = ConcurrentStack() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_label, k, _version)) -> + eventLog.Push (e, k) + eventTriggered.Set() |> ignore) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -237,24 +214,28 @@ let ``Job is restarted if first requestor cancels but keeps running if second re let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) - jobStarted.WaitOne() |> ignore - jobStarted.Reset() |> ignore + do! waitFor jobStarted + + do! waitFor eventTriggered + let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) + // Give the other tasks a chance to actually start. + do! Task.Yield() + let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) + do! Task.Yield() cts1.Cancel() - - jobStarted.WaitOne() |> ignore - cts2.Cancel() + cts3.Cancel() - let! result = _task3 - Assert.Equal(2, result) + // Wait for the event to be logged. + do! waitFor eventTriggered - Assert.Equal(TaskStatus.Canceled, _task1.Status) + jobCanComplete.Set() |> ignore - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList - let expected = [ Started, key; Started, key; Finished, key ] + let orderedLog = eventLog |> Seq.rev |> Seq.toList + let expected = [ Started, key; Canceled, key ] Assert.Equal<_ list>(expected, orderedLog) } @@ -390,14 +371,21 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = task { let cache = AsyncMemoize(cancelDuplicateRunningJobs=cancelDuplicate) + use jobCompleted = new AutoResetEvent(false) + use jobStarted = new AutoResetEvent(false) + let mutable started = 0 let mutable finished = 0 let work () = node { - Interlocked.Increment &started |> ignore - for _ in 1..10 do - do! Async.Sleep 10 |> NodeCode.AwaitAsync - Interlocked.Increment &finished |> ignore + jobStarted.Set() |> ignore + try + Interlocked.Increment &started |> ignore + for _ in 1..10 do + do! Async.Sleep 10 |> NodeCode.AwaitAsync + Interlocked.Increment &finished |> ignore + finally + jobCompleted.Set() |> ignore } let key1 = @@ -408,7 +396,7 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = cache.Get(key1, work()) |> Async.AwaitNodeCode |> Async.Start - do! Task.Delay 50 + do! waitFor jobStarted let key2 = { new ICacheKey<_, _> with @@ -418,7 +406,10 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = cache.Get(key2, work()) |> Async.AwaitNodeCode |> Async.Start - do! Task.Delay 500 + do! waitFor jobStarted + + do! waitFor jobCompleted + do! waitFor jobCompleted Assert.Equal((2, expectFinished), (started, finished)) } diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index a6b7f6fcc41..57a9e266fe8 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -219,13 +219,13 @@ let ``Changes in a referenced project`` () = [] let ``File is not checked twice`` () = - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() testWorkflow() { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Third" expectOk @@ -242,13 +242,13 @@ let ``File is not checked twice`` () = [] let ``If a file is checked as a dependency it's not re-checked later`` () = - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() testWorkflow() { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Last" expectOk @@ -272,13 +272,13 @@ let ``We don't check files that are not depended on`` () = sourceFile "Third" ["First"], sourceFile "Last" ["Third"]) - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Last" expectOk @@ -302,8 +302,8 @@ let ``Files that are not depended on don't invalidate cache`` () = sourceFile "Third" ["First"], sourceFile "Last" ["Third"]) - let cacheTcIntermediateEvents = ResizeArray() - let cacheGraphConstructionEvents = ResizeArray() + let cacheTcIntermediateEvents = ConcurrentQueue() + let cacheGraphConstructionEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "First" updatePublicSurface @@ -311,8 +311,8 @@ let ``Files that are not depended on don't invalidate cache`` () = withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Add - checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Enqueue + checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Enqueue }) updateFile "Second" updatePublicSurface @@ -344,8 +344,8 @@ let ``Files that are not depended on don't invalidate cache part 2`` () = sourceFile "D" ["B"; "C"], sourceFile "E" ["C"]) - let cacheTcIntermediateEvents = ResizeArray() - let cacheGraphConstructionEvents = ResizeArray() + let cacheTcIntermediateEvents = ConcurrentQueue() + let cacheGraphConstructionEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "A" updatePublicSurface @@ -353,8 +353,8 @@ let ``Files that are not depended on don't invalidate cache part 2`` () = withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Add - checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Enqueue + checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Enqueue }) updateFile "B" updatePublicSurface checkFile "E" expectOk @@ -382,7 +382,7 @@ let ``Changing impl files doesn't invalidate cache when they have signatures`` ( { sourceFile "B" ["A"] with SignatureFile = AutoGenerated }, { sourceFile "C" ["B"] with SignatureFile = AutoGenerated }) - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "A" updatePublicSurface @@ -390,7 +390,7 @@ let ``Changing impl files doesn't invalidate cache when they have signatures`` ( withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "A" updateInternal checkFile "C" expectOk @@ -412,14 +412,14 @@ let ``Changing impl file doesn't invalidate an in-memory referenced project`` () SyntheticProject.Create("project", sourceFile "B" ["A"] ) with DependsOn = [library] } - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { checkFile "B" expectOk withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "A" updateInternal checkFile "B" expectOk