From 718d3c4970011dc0d8253572adbf529c87623d75 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 22 Feb 2021 07:35:02 -0800 Subject: [PATCH] More reactor queue dependency removal (#11109) --- src/fsharp/CompilerConfig.fs | 6 +- src/fsharp/CompilerConfig.fsi | 4 +- src/fsharp/service/IncrementalBuild.fs | 407 ++++++++++++++---------- src/fsharp/service/IncrementalBuild.fsi | 10 +- src/fsharp/service/service.fs | 141 ++++---- 5 files changed, 324 insertions(+), 244 deletions(-) diff --git a/src/fsharp/CompilerConfig.fs b/src/fsharp/CompilerConfig.fs index 955b4ce3ed2..042df6f95a8 100644 --- a/src/fsharp/CompilerConfig.fs +++ b/src/fsharp/CompilerConfig.fs @@ -193,10 +193,10 @@ type TimeStampCache(defaultTimeStamp: DateTime) = files.[fileName] <- v v - member cache.GetProjectReferenceTimeStamp (pr: IProjectReference, ctok) = + member cache.GetProjectReferenceTimeStamp (pr: IProjectReference) = let ok, v = projects.TryGetValue pr if ok then v else - let v = defaultArg (pr.TryGetLogicalTimeStamp (cache, ctok)) defaultTimeStamp + let v = defaultArg (pr.TryGetLogicalTimeStamp (cache)) defaultTimeStamp projects.[pr] <- v v @@ -215,7 +215,7 @@ and IProjectReference = /// /// The operation returns None only if it is not possible to create an IncrementalBuilder for the project at all, e.g. if there /// are fatal errors in the options for the project. - abstract TryGetLogicalTimeStamp: TimeStampCache * CompilationThreadToken -> System.DateTime option + abstract TryGetLogicalTimeStamp: TimeStampCache -> System.DateTime option type AssemblyReference = | AssemblyReference of range * string * IProjectReference option diff --git a/src/fsharp/CompilerConfig.fsi b/src/fsharp/CompilerConfig.fsi index 58ce21a082b..5442a723133 100644 --- a/src/fsharp/CompilerConfig.fsi +++ b/src/fsharp/CompilerConfig.fsi @@ -58,7 +58,7 @@ type IRawFSharpAssemblyData = type TimeStampCache = new: defaultTimeStamp: DateTime -> TimeStampCache member GetFileTimeStamp: string -> DateTime - member GetProjectReferenceTimeStamp: IProjectReference * CompilationThreadToken -> DateTime + member GetProjectReferenceTimeStamp: IProjectReference -> DateTime and IProjectReference = @@ -76,7 +76,7 @@ and IProjectReference = /// /// The operation returns None only if it is not possible to create an IncrementalBuilder for the project at all, e.g. if there /// are fatal errors in the options for the project. - abstract TryGetLogicalTimeStamp: TimeStampCache * CompilationThreadToken -> System.DateTime option + abstract TryGetLogicalTimeStamp: TimeStampCache -> System.DateTime option type AssemblyReference = | AssemblyReference of range * string * IProjectReference option diff --git a/src/fsharp/service/IncrementalBuild.fs b/src/fsharp/service/IncrementalBuild.fs index 7e78e194f11..3bb1c330818 100644 --- a/src/fsharp/service/IncrementalBuild.fs +++ b/src/fsharp/service/IncrementalBuild.fs @@ -4,6 +4,7 @@ namespace FSharp.Compiler.CodeAnalysis open System open System.Collections.Generic +open System.Collections.Immutable open System.IO open System.Runtime.InteropServices open System.Threading @@ -230,6 +231,8 @@ type BoundModel private (tcConfig: TcConfig, syntaxTreeOpt: SyntaxTree option, lazyTcInfoState: TcInfoState option ref) = + let gate = obj() + let defaultTypeCheck () = eventually { match prevTcInfoOptional with @@ -258,18 +261,20 @@ type BoundModel private (tcConfig: TcConfig, None member this.Invalidate() = - let hasSig = this.BackingSignature.IsSome - match !lazyTcInfoState with - // If partial checking is enabled and we have a backing sig file, then do nothing. The partial state contains the sig state. - | Some(PartialState _) when enablePartialTypeChecking && hasSig -> () - // If partial checking is enabled and we have a backing sig file, then use the partial state. The partial state contains the sig state. - | Some(FullState(tcInfo, _)) when enablePartialTypeChecking && hasSig -> lazyTcInfoState := Some(PartialState tcInfo) - | _ -> - lazyTcInfoState := None + lock gate (fun () -> + let hasSig = this.BackingSignature.IsSome + match !lazyTcInfoState with + // If partial checking is enabled and we have a backing sig file, then do nothing. The partial state contains the sig state. + | Some(PartialState _) when enablePartialTypeChecking && hasSig -> () + // If partial checking is enabled and we have a backing sig file, then use the partial state. The partial state contains the sig state. + | Some(FullState(tcInfo, _)) when enablePartialTypeChecking && hasSig -> lazyTcInfoState := Some(PartialState tcInfo) + | _ -> + lazyTcInfoState := None - // Always invalidate the syntax tree cache. - syntaxTreeOpt - |> Option.iter (fun x -> x.Invalidate()) + // Always invalidate the syntax tree cache. + syntaxTreeOpt + |> Option.iter (fun x -> x.Invalidate()) + ) member this.GetState(partialCheck: bool) = let partialCheck = @@ -283,12 +288,10 @@ type BoundModel private (tcConfig: TcConfig, | Some(PartialState _), false -> true | _ -> false - if mustCheck then - lazyTcInfoState := None - match !lazyTcInfoState with - | Some tcInfoState -> tcInfoState |> Eventually.Done + | Some tcInfoState when not mustCheck -> tcInfoState |> Eventually.Done | _ -> + lazyTcInfoState := None eventually { let! tcInfoState = this.TypeCheck(partialCheck) lazyTcInfoState := Some tcInfoState @@ -359,6 +362,14 @@ type BoundModel private (tcConfig: TcConfig, return state.Partial } + member this.TryTcInfo = + match !lazyTcInfoState with + | Some(state) -> + match state with + | FullState(tcInfo, _) + | PartialState(tcInfo) -> Some tcInfo + | _ -> None + member this.TcInfoWithOptional = eventually { let! state = this.GetState(false) @@ -600,6 +611,8 @@ type PartialCheckResults private (boundModel: BoundModel, timeStamp: DateTime) = member _.TcInfo ctok = boundModel.TcInfo |> eval ctok + member _.TryTcInfo = boundModel.TryTcInfo + member _.TcInfoWithOptional ctok = boundModel.TcInfoWithOptional |> eval ctok member _.TryGetItemKeyStore ctok = @@ -653,6 +666,19 @@ type RawFSharpAssemblyDataBackedByLanguageService (tcConfig, tcGlobals, tcState: member _.HasAnyFSharpSignatureDataAttribute = true member _.HasMatchingFSharpSignatureDataAttribute _ilg = true +type IncrementalBuilderState = + { + // stampedFileNames represent the real stamps of the files. + // logicalStampedFileNames represent the stamps of the files that are used to calculate the project's logical timestamp. + stampedFileNames: ImmutableArray + logicalStampedFileNames: ImmutableArray + stampedReferencedAssemblies: ImmutableArray + initialBoundModel: BoundModel option + boundModels: ImmutableArray + finalizedBoundModel: ((ILAssemblyRef * IRawFSharpAssemblyData option * TypedImplFile list option * BoundModel) * DateTime) option + enablePartialTypeChecking: bool + } + /// Manages an incremental build graph for the build of a single F# project type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInputs, nonFrameworkResolutions, unresolvedReferences, tcConfig: TcConfig, projectDirectory, outfile, assemblyName, niceNameGen: NiceNameGenerator, lexResourceManager, @@ -673,7 +699,6 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput #endif let mutable currentTcImportsOpt = None let defaultPartialTypeChecking = enablePartialTypeChecking - let mutable enablePartialTypeChecking = enablePartialTypeChecking // Check for the existence of loaded sources and prepend them to the sources list if present. let sourceFiles = tcConfig.GetAvailableLoadedSources() @ (sourceFiles |>List.map (fun s -> rangeStartup, s)) @@ -711,7 +736,7 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput // START OF BUILD TASK FUNCTIONS /// Get the timestamp of the given file name. - let StampFileNameTask (cache: TimeStampCache) _ctok (_m: range, filename: string, _isLastCompiland) = + let StampFileNameTask (cache: TimeStampCache) (_m: range, filename: string, _isLastCompiland) = cache.GetFileTimeStamp filename /// Parse the given file and return the given input. @@ -719,8 +744,8 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput SyntaxTree(tcConfig, fileParsed, lexResourceManager, sourceRange, filename, isLastCompiland) /// Timestamps of referenced assemblies are taken from the file's timestamp. - let StampReferencedAssemblyTask (cache: TimeStampCache) ctok (_ref, timeStamper) = - timeStamper cache ctok + let StampReferencedAssemblyTask (cache: TimeStampCache) (_ref, timeStamper) = + timeStamper cache // Link all the assemblies together and produce the input typecheck accumulator let CombineImportedAssembliesTask ctok : Cancellable = @@ -808,7 +833,7 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput beforeFileChecked, fileChecked, tcInfo, Eventually.Done (Some tcInfoOptional), None) } /// Type check all files. - let TypeCheckTask ctok (prevBoundModel: BoundModel) syntaxTree: Eventually = + let TypeCheckTask ctok enablePartialTypeChecking (prevBoundModel: BoundModel) syntaxTree: Eventually = eventually { RequireCompilationThread ctok let! boundModel = prevBoundModel.Next(syntaxTree) @@ -819,7 +844,7 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput } /// Finish up the typechecking to produce outputs for the rest of the compilation process - let FinalizeTypeCheckTask ctok (boundModels: BoundModel[]) = + let FinalizeTypeCheckTask ctok enablePartialTypeChecking (boundModels: ImmutableArray) = cancellable { DoesNotRequireCompilerThreadTokenAndCouldPossiblyBeMadeConcurrent ctok @@ -835,7 +860,7 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput let (_tcEnvAtEndOfLastFile, topAttrs, mimpls, _), tcState = let results = boundModels - |> List.ofArray + |> List.ofSeq |> List.map (fun boundModel -> let tcInfo, latestImplFile = if enablePartialTypeChecking then @@ -916,59 +941,60 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput let fileNames = sourceFiles |> Array.ofList // TODO: This should be an immutable array. let referencedAssemblies = nonFrameworkAssemblyInputs |> Array.ofList // TODO: This should be an immutable array. - (* - The data below represents a dependency graph. - - ReferencedAssembliesStamps => FileStamps => BoundModels => FinalizedBoundModel - *) - - // stampedFileNames represent the real stamps of the files. - // logicalStampedFileNames represent the stamps of the files that are used to calculate the project's logical timestamp. - let stampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) - let logicalStampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) - let stampedReferencedAssemblies = Array.init referencedAssemblies.Length (fun _ -> DateTime.MinValue) - let mutable initialBoundModel = None - let boundModels = Array.zeroCreate fileNames.Length - let mutable finalizedBoundModel = None - - let computeStampedFileName (cache: TimeStampCache) (ctok: CompilationThreadToken) slot fileInfo cont = - let currentStamp = stampedFileNames.[slot] - let stamp = StampFileNameTask cache ctok fileInfo + let computeStampedFileName (state: IncrementalBuilderState) (cache: TimeStampCache) slot fileInfo = + let currentStamp = state.stampedFileNames.[slot] + let stamp = StampFileNameTask cache fileInfo if currentStamp <> stamp then - match boundModels.[slot] with + match state.boundModels.[slot] with // This prevents an implementation file that has a backing signature file from invalidating the rest of the build. - | Some(boundModel) when enablePartialTypeChecking && boundModel.BackingSignature.IsSome -> - stampedFileNames.[slot] <- StampFileNameTask cache ctok fileInfo + | Some(boundModel) when state.enablePartialTypeChecking && boundModel.BackingSignature.IsSome -> boundModel.Invalidate() + { state with + stampedFileNames = state.stampedFileNames.SetItem(slot, StampFileNameTask cache fileInfo) + } | _ -> - // Something changed, the finalized view of the project must be invalidated. - finalizedBoundModel <- None + + let stampedFileNames = state.stampedFileNames.ToBuilder() + let logicalStampedFileNames = state.logicalStampedFileNames.ToBuilder() + let boundModels = state.boundModels.ToBuilder() // Invalidate the file and all files below it. - stampedFileNames.[slot..] - |> Array.iteri (fun j _ -> - let stamp = StampFileNameTask cache ctok fileNames.[slot + j] + for j = 0 to stampedFileNames.Count - slot - 1 do + let stamp = StampFileNameTask cache fileNames.[slot + j] stampedFileNames.[slot + j] <- stamp logicalStampedFileNames.[slot + j] <- stamp boundModels.[slot + j] <- None - ) - if boundModels.[slot].IsNone then - cont slot fileInfo + { state with + // Something changed, the finalized view of the project must be invalidated. + finalizedBoundModel = None - let computeStampedFileNames (cache: TimeStampCache) (ctok: CompilationThreadToken) = - fileNames - |> Array.iteri (fun i fileInfo -> - computeStampedFileName cache ctok i fileInfo (fun _ _ -> ()) + stampedFileNames = stampedFileNames.ToImmutable() + logicalStampedFileNames = logicalStampedFileNames.ToImmutable() + boundModels = boundModels.ToImmutable() + } + else + state + + let computeStampedFileNames state (cache: TimeStampCache) = + let mutable i = 0 + (state, fileNames) + ||> Array.fold (fun state fileInfo -> + let newState = computeStampedFileName state cache i fileInfo + i <- i + 1 + newState ) - let computeStampedReferencedAssemblies (cache: TimeStampCache) (ctok: CompilationThreadToken) = + let computeStampedReferencedAssemblies state (cache: TimeStampCache) = + let stampedReferencedAssemblies = state.stampedReferencedAssemblies.ToBuilder() + let mutable referencesUpdated = false referencedAssemblies |> Array.iteri (fun i asmInfo -> - let currentStamp = stampedReferencedAssemblies.[i] - let stamp = StampReferencedAssemblyTask cache ctok asmInfo + + let currentStamp = state.stampedReferencedAssemblies.[i] + let stamp = StampReferencedAssemblyTask cache asmInfo if currentStamp <> stamp then referencesUpdated <- true @@ -978,154 +1004,189 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput if referencesUpdated then // Something changed, the finalized view of the project must be invalidated. // This is the only place where the initial bound model will be invalidated. - initialBoundModel <- None - finalizedBoundModel <- None - - for i = 0 to stampedFileNames.Length - 1 do - stampedFileNames.[i] <- DateTime.MinValue - logicalStampedFileNames.[i] <- DateTime.MinValue - boundModels.[i] <- None - - let getStampedFileNames cache ctok = - computeStampedFileNames cache ctok - logicalStampedFileNames - - let getStampedReferencedAssemblies cache ctok = - computeStampedReferencedAssemblies cache ctok - stampedReferencedAssemblies + let count = state.stampedFileNames.Length + { state with + stampedReferencedAssemblies = stampedReferencedAssemblies.ToImmutable() + initialBoundModel = None + finalizedBoundModel = None + stampedFileNames = Array.init count (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + logicalStampedFileNames = Array.init count (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + boundModels = Array.init count (fun _ -> None) |> ImmutableArray.CreateRange + } + else + { state with + stampedReferencedAssemblies = stampedReferencedAssemblies.ToImmutable() + } - let computeInitialBoundModel (ctok: CompilationThreadToken) = + let computeInitialBoundModel (state: IncrementalBuilderState) (ctok: CompilationThreadToken) = cancellable { - match initialBoundModel with + match state.initialBoundModel with | None -> let! result = CombineImportedAssembliesTask ctok - initialBoundModel <- Some result - return result + return { state with initialBoundModel = Some result }, result | Some result -> - return result + return state, result } - let computeBoundModel (cache: TimeStampCache) (ctok: CompilationThreadToken) (slot: int) = + let computeBoundModel state (cache: TimeStampCache) (ctok: CompilationThreadToken) (slot: int) = if IncrementalBuild.injectCancellationFault then Cancellable.canceled () else cancellable { - let! initial = computeInitialBoundModel ctok + let! (state, initial) = computeInitialBoundModel state ctok let fileInfo = fileNames.[slot] - computeStampedFileName cache ctok slot fileInfo (fun slot fileInfo -> - let prevBoundModel = - match slot with - | 0 (* first file *) -> initial - | _ -> - match boundModels.[slot - 1] with - | Some(prevBoundModel) -> prevBoundModel - | _ -> - // This shouldn't happen, but on the off-chance, just grab the initial bound model. - initial - - let boundModel = TypeCheckTask ctok prevBoundModel (ParseTask fileInfo) |> Eventually.force ctok - - boundModels.[slot] <- Some boundModel - ) + let state = computeStampedFileName state cache slot fileInfo + + let state = + if state.boundModels.[slot].IsNone then + let prevBoundModel = + match slot with + | 0 (* first file *) -> initial + | _ -> + match state.boundModels.[slot - 1] with + | Some(prevBoundModel) -> prevBoundModel + | _ -> + // This shouldn't happen, but on the off-chance, just grab the initial bound model. + initial + + let boundModel = TypeCheckTask ctok state.enablePartialTypeChecking prevBoundModel (ParseTask fileInfo) |> Eventually.force ctok + + { state with + boundModels = state.boundModels.SetItem(slot, Some boundModel) + } + else + state + + return state } - let computeBoundModels (cache: TimeStampCache) (ctok: CompilationThreadToken) = + let computeBoundModels state (cache: TimeStampCache) (ctok: CompilationThreadToken) = + let mutable state = state + let task = + cancellable { + for slot = 0 to fileNames.Length - 1 do + let! newState = computeBoundModel state cache ctok slot + state <- newState + } cancellable { - for slot = 0 to fileNames.Length - 1 do - do! computeBoundModel cache ctok slot + let! _ = task + return state } - let computeFinalizedBoundModel (cache: TimeStampCache) (ctok: CompilationThreadToken) = + let computeFinalizedBoundModel state (cache: TimeStampCache) (ctok: CompilationThreadToken) = cancellable { - let! _ = computeBoundModels cache ctok + let! state = computeBoundModels state cache ctok - match finalizedBoundModel with - | Some result -> return result + match state.finalizedBoundModel with + | Some result -> return state, result | _ -> - let boundModels = boundModels |> Array.choose id + let boundModels = state.boundModels |> Seq.choose id |> ImmutableArray.CreateRange - let! result = FinalizeTypeCheckTask ctok boundModels + let! result = FinalizeTypeCheckTask ctok state.enablePartialTypeChecking boundModels let result = (result, DateTime.UtcNow) - finalizedBoundModel <- Some result - return result + return { state with finalizedBoundModel = Some result }, result } - let step (cache: TimeStampCache) (ctok: CompilationThreadToken) = + let step state (cache: TimeStampCache) (ctok: CompilationThreadToken) = cancellable { - computeStampedReferencedAssemblies cache ctok - computeStampedFileNames cache ctok + let state = computeStampedReferencedAssemblies state cache + let state = computeStampedFileNames state cache - match boundModels |> Array.tryFindIndex (fun x -> x.IsNone) with + match state.boundModels |> Seq.tryFindIndex (fun x -> x.IsNone) with | Some slot -> - do! computeBoundModel cache ctok slot - return true + let! state = computeBoundModel state cache ctok slot + return state, true | _ -> - return false + return state, false } - let tryGetBeforeSlot slot = + let tryGetBeforeSlot (state: IncrementalBuilderState) slot = match slot with | 0 (* first file *) -> - match initialBoundModel with + match state.initialBoundModel with | Some initial -> (initial, DateTime.MinValue) |> Some | _ -> None | _ -> - match boundModels.[slot - 1] with + match state.boundModels.[slot - 1] with | Some boundModel -> - (boundModel, stampedFileNames.[slot - 1]) + (boundModel, state.stampedFileNames.[slot - 1]) |> Some | _ -> None - let eval cache ctok targetSlot = + let eval state (cache: TimeStampCache) ctok targetSlot = if targetSlot < 0 then cancellable { - computeStampedReferencedAssemblies cache ctok + let state = computeStampedReferencedAssemblies state cache - let! result = computeInitialBoundModel ctok - return Some(result, DateTime.MinValue) + let! state, result = computeInitialBoundModel state ctok + return state, Some(result, DateTime.MinValue) } else + let mutable state = state let evalUpTo = cancellable { for slot = 0 to targetSlot do - do! computeBoundModel cache ctok slot + let! newState = computeBoundModel state cache ctok slot + state <- newState } cancellable { - computeStampedReferencedAssemblies cache ctok + let newState = computeStampedReferencedAssemblies state cache + state <- newState let! _ = evalUpTo - return - boundModels.[targetSlot] + let result = + state.boundModels.[targetSlot] |> Option.map (fun boundModel -> - (boundModel, stampedFileNames.[targetSlot]) + (boundModel, state.stampedFileNames.[targetSlot]) ) + + return state, result } - let tryGetFinalized cache ctok = + let tryGetFinalized state cache ctok = cancellable { - computeStampedReferencedAssemblies cache ctok + let state = computeStampedReferencedAssemblies state cache - let! res = computeFinalizedBoundModel cache ctok - return Some res + let! state, res = computeFinalizedBoundModel state cache ctok + return state, Some res } - let MaxTimeStampInDependencies cache (ctok: CompilationThreadToken) getStamps = - let stamps = getStamps cache ctok - if Array.isEmpty stamps then + let MaxTimeStampInDependencies stamps = + if Seq.isEmpty stamps then DateTime.MinValue else stamps - |> Array.max + |> Seq.max // END OF BUILD DESCRIPTION - // --------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- + + (* + The data below represents a dependency graph. + + ReferencedAssembliesStamps => FileStamps => BoundModels => FinalizedBoundModel + *) + + let mutable currentState = + { + stampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + logicalStampedFileNames = Array.init fileNames.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + stampedReferencedAssemblies = Array.init referencedAssemblies.Length (fun _ -> DateTime.MinValue) |> ImmutableArray.CreateRange + initialBoundModel = None + boundModels = Array.zeroCreate fileNames.Length |> ImmutableArray.CreateRange + finalizedBoundModel = None + enablePartialTypeChecking = enablePartialTypeChecking + } + + let setCurrentState (_ctok: CompilationThreadToken) state = + currentState <- state do IncrementalBuilderEventTesting.MRU.Add(IncrementalBuilderEventTesting.IBECreated) @@ -1150,7 +1211,8 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput member _.Step (ctok: CompilationThreadToken) = cancellable { let cache = TimeStampCache defaultTimeStamp // One per step - let! res = step cache ctok + let! state, res = step currentState cache ctok + setCurrentState ctok state if not res then projectChecked.Trigger() return false @@ -1160,7 +1222,7 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput member builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename: PartialCheckResults option = let slotOfFile = builder.GetSlotOfFileName filename - let result = tryGetBeforeSlot slotOfFile + let result = tryGetBeforeSlot currentState slotOfFile match result with | Some (boundModel, timestamp) -> Some (PartialCheckResults.Create (boundModel, timestamp)) @@ -1169,19 +1231,33 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput member builder.AreCheckResultsBeforeFileInProjectReady filename = let slotOfFile = builder.GetSlotOfFileName filename - match tryGetBeforeSlot slotOfFile with + match tryGetBeforeSlot currentState slotOfFile with | Some _ -> true | _ -> false - - member _.GetCheckResultsBeforeSlotInProject (ctok: CompilationThreadToken, slotOfFile) = + + member builder.TryGetCheckResultsBeforeFileInProject (filename) = + let cache = TimeStampCache defaultTimeStamp + let state = currentState + let state = computeStampedFileNames state cache + let state = computeStampedReferencedAssemblies state cache + + let slotOfFile = builder.GetSlotOfFileName filename + match tryGetBeforeSlot state slotOfFile with + | Some(boundModel, timestamp) -> PartialCheckResults.Create(boundModel, timestamp) |> Some + | _ -> None + + member private _.GetCheckResultsBeforeSlotInProject (ctok: CompilationThreadToken, slotOfFile, enablePartialTypeChecking) = cancellable { let cache = TimeStampCache defaultTimeStamp - let! result = eval cache ctok (slotOfFile - 1) - + let! state, result = eval { currentState with enablePartialTypeChecking = enablePartialTypeChecking } cache ctok (slotOfFile - 1) + setCurrentState ctok { state with enablePartialTypeChecking = defaultPartialTypeChecking } match result with | Some (boundModel, timestamp) -> return PartialCheckResults.Create (boundModel, timestamp) | None -> return! failwith "Build was not evaluated, expected the results to be ready after 'Eval' (GetCheckResultsBeforeSlotInProject)." } + + member builder.GetCheckResultsBeforeSlotInProject (ctok: CompilationThreadToken, slotOfFile) = + builder.GetCheckResultsBeforeSlotInProject(ctok, slotOfFile, defaultPartialTypeChecking) member builder.GetCheckResultsBeforeFileInProject (ctok: CompilationThreadToken, filename) = let slotOfFile = builder.GetSlotOfFileName filename @@ -1192,24 +1268,23 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput builder.GetCheckResultsBeforeSlotInProject (ctok, slotOfFile) member builder.GetFullCheckResultsAfterFileInProject (ctok: CompilationThreadToken, filename) = - enablePartialTypeChecking <- false cancellable { - try - let! result = builder.GetCheckResultsAfterFileInProject(ctok, filename) - result.TcInfoWithOptional ctok |> ignore // Make sure we forcefully evaluate the info - return result - finally - enablePartialTypeChecking <- defaultPartialTypeChecking + let slotOfFile = builder.GetSlotOfFileName filename + 1 + let! result = builder.GetCheckResultsBeforeSlotInProject(ctok, slotOfFile, false) + result.TcInfoWithOptional ctok |> ignore // Make sure we forcefully evaluate the info + return result } member builder.GetCheckResultsAfterLastFileInProject (ctok: CompilationThreadToken) = builder.GetCheckResultsBeforeSlotInProject(ctok, builder.GetSlotsCount()) - member _.GetCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = + member private _.GetCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken, enablePartialTypeChecking) = cancellable { let cache = TimeStampCache defaultTimeStamp - match! tryGetFinalized cache ctok with + let! state, result = tryGetFinalized { currentState with enablePartialTypeChecking = enablePartialTypeChecking } cache ctok + setCurrentState ctok { state with enablePartialTypeChecking = defaultPartialTypeChecking } + match result with | Some ((ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt, boundModel), timestamp) -> return PartialCheckResults.Create (boundModel, timestamp), ilAssemRef, tcAssemblyDataOpt, tcAssemblyExprOpt | None -> @@ -1217,21 +1292,23 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput return! failwith msg } - member this.GetFullCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = - enablePartialTypeChecking <- false + member builder.GetCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = + builder.GetCheckResultsAndImplementationsForProject(ctok, defaultPartialTypeChecking) + + member builder.GetFullCheckResultsAndImplementationsForProject(ctok: CompilationThreadToken) = cancellable { - try - let! result = this.GetCheckResultsAndImplementationsForProject(ctok) - let results, _, _, _ = result - results.TcInfoWithOptional ctok |> ignore // Make sure we forcefully evaluate the info - return result - finally - enablePartialTypeChecking <- defaultPartialTypeChecking + let! result = builder.GetCheckResultsAndImplementationsForProject(ctok, false) + let results, _, _, _ = result + results.TcInfoWithOptional ctok |> ignore // Make sure we forcefully evaluate the info + return result } - member _.GetLogicalTimeStampForProject(cache, ctok: CompilationThreadToken) = - let t1 = MaxTimeStampInDependencies cache ctok getStampedReferencedAssemblies - let t2 = MaxTimeStampInDependencies cache ctok getStampedFileNames + member _.GetLogicalTimeStampForProject(cache) = + let state = currentState + let state = computeStampedFileNames state cache + let state = computeStampedReferencedAssemblies state cache + let t1 = MaxTimeStampInDependencies state.stampedReferencedAssemblies + let t2 = MaxTimeStampInDependencies state.stampedFileNames max t1 t2 member _.TryGetSlotOfFileName(filename: string) = @@ -1402,10 +1479,10 @@ type IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInput [ for r in nonFrameworkResolutions do let fileName = r.resolvedPath - yield (Choice1Of2 fileName, (fun (cache: TimeStampCache) _ctok -> cache.GetFileTimeStamp fileName)) + yield (Choice1Of2 fileName, (fun (cache: TimeStampCache) -> cache.GetFileTimeStamp fileName)) for pr in projectReferences do - yield Choice2Of2 pr, (fun (cache: TimeStampCache) ctok -> cache.GetProjectReferenceTimeStamp (pr, ctok)) ] + yield Choice2Of2 pr, (fun (cache: TimeStampCache) -> cache.GetProjectReferenceTimeStamp (pr)) ] let builder = new IncrementalBuilder(tcGlobals, frameworkTcImports, nonFrameworkAssemblyInputs, diff --git a/src/fsharp/service/IncrementalBuild.fsi b/src/fsharp/service/IncrementalBuild.fsi index 15fd2e1d2f3..60d73de2c2c 100755 --- a/src/fsharp/service/IncrementalBuild.fsi +++ b/src/fsharp/service/IncrementalBuild.fsi @@ -106,6 +106,8 @@ type internal PartialCheckResults = member TcInfo: CompilationThreadToken -> TcInfo + member TryTcInfo: TcInfo option + /// Can cause a second type-check if `enablePartialTypeChecking` is true in the checker. /// Only use when it's absolutely necessary to get rich information on a file. member TcInfoWithOptional: CompilationThreadToken -> TcInfo * TcInfoOptional @@ -173,6 +175,12 @@ type internal IncrementalBuilder = /// This is safe for use from non-compiler threads member AreCheckResultsBeforeFileInProjectReady: filename:string -> bool + /// Get the preceding typecheck state of a slot, WITH checking if it is up-to-date w.r.t. However, files will not be parsed or checked. + /// the timestamps on files and referenced DLLs prior to this one. Return None if the result is not available or if it is not up-to-date. + /// + /// This is safe for use from non-compiler threads but the objects returned must in many cases be accessed only from the compiler thread. + member TryGetCheckResultsBeforeFileInProject: filename: string -> PartialCheckResults option + /// Get the preceding typecheck state of a slot. Compute the entire type check of the project up /// to the necessary point if the result is not available. This may be a long-running operation. /// @@ -212,7 +220,7 @@ type internal IncrementalBuilder = member GetFullCheckResultsAndImplementationsForProject : CompilationThreadToken -> Cancellable /// Get the logical time stamp that is associated with the output of the project if it were gully built immediately - member GetLogicalTimeStampForProject: TimeStampCache * CompilationThreadToken -> DateTime + member GetLogicalTimeStampForProject: TimeStampCache -> DateTime /// Does the given file exist in the builder's pipeline? member ContainsFile: filename: string -> bool diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 42bcbc9bf3c..2e521fbc85f 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -303,8 +303,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "GetAssemblyData", nm) return! self.GetAssemblyData(opts, ctok, userOpName + ".CheckReferencedProject("+nm+")") } - member x.TryGetLogicalTimeStamp(cache, ctok) = - self.TryGetLogicalTimeStampForProject(cache, ctok, opts, userOpName + ".TimeStampReferencedProject("+nm+")") + member x.TryGetLogicalTimeStamp(cache) = + self.TryGetLogicalTimeStampForProject(cache, opts) member x.FileName = nm } ] let loadClosure = scriptClosureCache.TryGet(AnyCallerThread, options) @@ -389,6 +389,24 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC else getSimilarOrCreateBuilder (ctok, options, userOpName) + let getAnyBuilder (reactor: Reactor) (options, userOpName, opName, opArg) = + let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, opName, opArg, action) + match tryGetAnyBuilder options with + | Some (builderOpt,creationDiags) -> + Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache + async { return builderOpt,creationDiags } + | _ -> + execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) + + let getBuilder (reactor: Reactor) (options, userOpName, opName, opArg) = + let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, opName, opArg, action) + match tryGetBuilder options with + | Some (builderOpt,creationDiags) -> + Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache + async { return builderOpt,creationDiags } + | _ -> + execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) + let parseCacheLock = Lock() // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.parseFileInProjectCache. Most recently used cache for parsing files. @@ -460,17 +478,8 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// Fetch the parse information from the background compiler (which checks w.r.t. the FileSystem API) member _.GetBackgroundParseResultsForFileInProject(filename, options, userOpName) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "GetBackgroundParseResultsForFileInProject ", filename, action) - let getBuilder options = - match tryGetBuilder options with - | Some (builderOpt,creationDiags) -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - async { return builderOpt,creationDiags } - | _ -> - execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) - async { - let! builderOpt, creationDiags = getBuilder options + let! builderOpt, creationDiags = getBuilder reactor (options, userOpName, "GetBackgroundParseResultsForFileInProject ", filename) match builderOpt with | None -> return FSharpParseFileResults(creationDiags, None, true, [| |]) | Some builder -> @@ -484,7 +493,6 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC let cachedResults = parseCacheLock.AcquireLock (fun ltok -> checkFileInProjectCache.TryGet(ltok, (filename, sourceText.GetHashCode(), options))) match cachedResults with -// | Some (parseResults, checkResults, _, _) when builder.AreCheckResultsBeforeFileInProjectReady(filename) -> | Some (parseResults, checkResults,_,priorTimeStamp) when (match builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename with @@ -583,41 +591,36 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// Type-check the result obtained by parsing, but only if the antecedent type checking context is available. member bc.CheckFileInProjectAllowingStaleCachedResults(parseResults: FSharpParseFileResults, filename, fileVersion, sourceText: ISourceText, options, userOpName) = - let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "CheckFileInProjectAllowingStaleCachedResults ", filename, action) async { try if implicitlyStartBackgroundWork then reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done let! cachedResults = - execWithReactorAsync <| fun ctok -> - cancellable { - let! _builderOpt,_creationErrors = getOrCreateBuilder (ctok, options, userOpName) - - match tryGetAnyBuilder options with - | Some (Some builder, creationDiags) -> - match bc.GetCachedCheckFileResult(builder, filename, sourceText, options) with - | Some (_, checkResults) -> return Some (builder, creationDiags, Some (FSharpCheckFileAnswer.Succeeded checkResults)) - | _ -> return Some (builder, creationDiags, None) - | _ -> return None // the builder wasn't ready - } + async { + let! builderOpt, creationDiags = getAnyBuilder reactor (options, userOpName, "CheckFileInProjectAllowingStaleCachedResults ", filename) + + match builderOpt with + | Some builder -> + match bc.GetCachedCheckFileResult(builder, filename, sourceText, options) with + | Some (_, checkResults) -> return Some (builder, creationDiags, Some (FSharpCheckFileAnswer.Succeeded checkResults)) + | _ -> return Some (builder, creationDiags, None) + | _ -> return None // the builder wasn't ready + } match cachedResults with | None -> return None | Some (_, _, Some x) -> return Some x | Some (builder, creationDiags, None) -> Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CheckFileInProjectAllowingStaleCachedResults.CacheMiss", filename) - let! tcPrior = - execWithReactorAsync <| fun ctok -> - cancellable { - DoesNotRequireCompilerThreadTokenAndCouldPossiblyBeMadeConcurrent ctok - let tcPrior = builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename - return - tcPrior - |> Option.map (fun tcPrior -> - (tcPrior, tcPrior.TcInfo ctok) - ) - } + let tcPrior = + let tcPrior = builder.GetCheckResultsBeforeFileInProjectEvenIfStale filename + tcPrior + |> Option.bind (fun tcPrior -> + match tcPrior.TryTcInfo with + | Some(tcInfo) -> Some (tcPrior, tcInfo) + | _ -> None + ) match tcPrior with | Some(tcPrior, tcInfo) -> @@ -631,19 +634,12 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// Type-check the result obtained by parsing. Force the evaluation of the antecedent type checking context if needed. member bc.CheckFileInProject(parseResults: FSharpParseFileResults, filename, fileVersion, sourceText: ISourceText, options, userOpName) = let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "CheckFileInProject", filename, action) - let getBuilder options = - match tryGetBuilder options with - | Some (builderOpt,creationDiags) -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - async { return builderOpt,creationDiags } - | _ -> - execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) async { try if implicitlyStartBackgroundWork then reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done - let! builderOpt,creationDiags = getBuilder options + let! builderOpt,creationDiags = getBuilder reactor (options, userOpName, "CheckFileInProject", filename) match builderOpt with | None -> return FSharpCheckFileAnswer.Succeeded (FSharpCheckFileResults.MakeEmpty(filename, creationDiags, keepAssemblyContents)) | Some builder -> @@ -653,13 +649,18 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC match cachedResults with | Some (_, checkResults) -> return FSharpCheckFileAnswer.Succeeded checkResults | _ -> - Trace.TraceInformation("FCS: {0}.{1} ({2})", userOpName, "CheckFileInProject.CacheMiss", filename) + // In order to prevent blocking of the reactor thread of getting a prior file, we try to get the results if it is considered up-to-date. + // If it's not up-to-date, then use the reactor thread to evaluate and get the results. let! tcPrior, tcInfo = - execWithReactorAsync <| fun ctok -> - cancellable { - let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) - return (tcPrior, tcPrior.TcInfo ctok) - } + match builder.TryGetCheckResultsBeforeFileInProject filename with + | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> + async { return (tcPrior, tcPrior.TryTcInfo.Value) } + | _ -> + execWithReactorAsync <| fun ctok -> + cancellable { + let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) + return (tcPrior, tcPrior.TcInfo ctok) + } let! checkAnswer = bc.CheckOneFileImpl(parseResults, sourceText, filename, options, fileVersion, builder, tcPrior.TcConfig, tcPrior.TcGlobals, tcPrior.TcImports, tcInfo.tcDependencyFiles, tcPrior.TimeStamp, tcInfo.tcState, tcInfo.moduleNamesDict, tcInfo.TcErrors, creationDiags, userOpName) return checkAnswer finally @@ -669,13 +670,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC /// Parses and checks the source file and returns untyped AST and check results. member bc.ParseAndCheckFileInProject (filename:string, fileVersion, sourceText: ISourceText, options:FSharpProjectOptions, userOpName) = let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "ParseAndCheckFileInProject", filename, action) - let getBuilder options = - match tryGetBuilder options with - | Some (builderOpt,creationDiags) -> - Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache - async { return builderOpt,creationDiags } - | _ -> - execWithReactorAsync (fun ctok -> getOrCreateBuilder (ctok, options, userOpName)) + async { try let strGuid = "_ProjectId=" + (options.ProjectId |> Option.defaultValue "null") @@ -685,7 +680,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC Logger.LogMessage (filename + strGuid + "-Cancelling background work") LogCompilerFunctionId.Service_ParseAndCheckFileInProject reactor.CancelBackgroundOp() // cancel the background work, since we will start new work after we're done - let! builderOpt,creationDiags = getBuilder options + let! builderOpt,creationDiags = getBuilder reactor (options, userOpName, "ParseAndCheckFileInProject", filename) match builderOpt with | None -> Logger.LogBlockMessageStop (filename + strGuid + "-Failed_Aborted") LogCompilerFunctionId.Service_ParseAndCheckFileInProject @@ -702,14 +697,18 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC return parseResults, FSharpCheckFileAnswer.Succeeded checkResults | _ -> - // todo this blocks the Reactor queue until all files up to the current are type checked. It's OK while editing the file, - // but results with non cooperative blocking when a firts file from a project opened. + // In order to prevent blocking of the reactor thread of getting a prior file, we try to get the results if it is considered up-to-date. + // If it's not up-to-date, then use the reactor thread to evaluate and get the results. let! tcPrior, tcInfo = - execWithReactorAsync <| fun ctok -> - cancellable { - let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) - return (tcPrior, tcPrior.TcInfo ctok) - } + match builder.TryGetCheckResultsBeforeFileInProject filename with + | Some(tcPrior) when tcPrior.TryTcInfo.IsSome -> + async { return (tcPrior, tcPrior.TryTcInfo.Value) } + | _ -> + execWithReactorAsync <| fun ctok -> + cancellable { + let! tcPrior = builder.GetCheckResultsBeforeFileInProject (ctok, filename) + return (tcPrior, tcPrior.TcInfo ctok) + } // Do the parsing. let parsingOptions = FSharpParsingOptions.FromTcConfig(builder.TcConfig, Array.ofList (builder.SourceFiles), options.UseScriptResolutionRules) @@ -860,14 +859,10 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC } /// Get the timestamp that would be on the output if fully built immediately - member private _.TryGetLogicalTimeStampForProject(cache, ctok, options, userOpName: string) = - - // NOTE: This creation of the background builder is currently run as uncancellable. Creating background builders is generally - // cheap though the timestamp computations look suspicious for transitive project references. - let builderOpt,_creationErrors = getOrCreateBuilder (ctok, options, userOpName + ".TryGetLogicalTimeStampForProject") |> Cancellable.runWithoutCancellation - match builderOpt with - | None -> None - | Some builder -> Some (builder.GetLogicalTimeStampForProject(cache, ctok)) + member private _.TryGetLogicalTimeStampForProject(cache, options) = + match tryGetBuilder options with + | Some (Some builder, _) -> Some (builder.GetLogicalTimeStampForProject(cache)) + | _ -> None /// Parse and typecheck the whole project.