Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/builder-caches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: IncrementalBuilder caches
category: Language Service Internals
categoryindex: 300
index: 1300
---
# IncrementalBuilder SyntaxTree cache

Incremental builder keeps in a cache at most one `ParsedInput` for each file it parses.
This behavior can be toggled with `useSyntaxTreeCache` parameter.

Memory impact of this feature can be in range of tens of MB for larger solutions. This can be inspected in memory profilng tools by searching for `ParsedInput` instances.
When partial checking is enabled, implementation files backed by signature will not be parsed or cached, as expected.
11 changes: 8 additions & 3 deletions src/Compiler/Facilities/BuildGraph.fs
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,14 @@ module GraphNode =
| None -> ()

[<Sealed>]
type GraphNode<'T>(retryCompute: bool, computation: NodeCode<'T>) =
type GraphNode<'T> private (retryCompute: bool, computation: NodeCode<'T>, cachedResult: Task<'T>, cachedResultNode: NodeCode<'T>) =

let gate = obj ()
let mutable computation = computation
let mutable requestCount = 0

let mutable cachedResult: Task<'T> = Unchecked.defaultof<_>
let mutable cachedResultNode: NodeCode<'T> = Unchecked.defaultof<_>
let mutable cachedResult: Task<'T> = cachedResult
let mutable cachedResultNode: NodeCode<'T> = cachedResultNode

let isCachedResultNodeNotNull () =
not (obj.ReferenceEquals(cachedResultNode, null))
Expand Down Expand Up @@ -429,4 +429,9 @@ type GraphNode<'T>(retryCompute: bool, computation: NodeCode<'T>) =

member _.IsComputing = requestCount > 0

static member FromResult(result: 'T) =
let nodeResult = node.Return result
GraphNode(true, nodeResult, Task.FromResult(result), nodeResult)

new(retryCompute: bool, computation) = GraphNode(retryCompute, computation, Unchecked.defaultof<_>, Unchecked.defaultof<_>)
new(computation) = GraphNode(retryCompute = true, computation = computation)
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/BuildGraph.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ type internal GraphNode<'T> =
/// By default, 'retryCompute' is 'true'.
new: computation: NodeCode<'T> -> GraphNode<'T>

/// Creates a GraphNode with given result already cached.
static member FromResult: 'T -> GraphNode<'T>

/// Return NodeCode which, when executed, will get the value of the computation if already computed, or
/// await an existing in-progress computation for the node if one exists, or else will synchronously
/// start the computation on the current thread.
Expand Down
285 changes: 148 additions & 137 deletions src/Compiler/Service/IncrementalBuild.fs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Compiler/Service/IncrementalBuild.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ type internal IncrementalBuilder =
parallelReferenceResolution: ParallelReferenceResolution *
captureIdentifiersWhenParsing: bool *
getSource: (string -> ISourceText option) option *
useChangeNotifications: bool ->
useChangeNotifications: bool *
useSyntaxTreeCache: bool ->
NodeCode<IncrementalBuilder option * FSharpDiagnostic[]>

/// Generalized Incremental Builder. This is exposed only for unit testing purposes.
Expand Down
25 changes: 15 additions & 10 deletions src/Compiler/Service/service.fs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ type BackgroundCompiler
parallelReferenceResolution,
captureIdentifiersWhenParsing,
getSource: (string -> ISourceText option) option,
useChangeNotifications
useChangeNotifications,
useSyntaxTreeCache
) as self =

let beforeFileChecked = Event<string * FSharpProjectOptions>()
Expand Down Expand Up @@ -329,7 +330,8 @@ type BackgroundCompiler
parallelReferenceResolution,
captureIdentifiersWhenParsing,
getSource,
useChangeNotifications
useChangeNotifications,
useSyntaxTreeCache
)

match builderOpt with
Expand Down Expand Up @@ -408,7 +410,7 @@ type BackgroundCompiler
let createBuilderNode (options, userOpName, ct: CancellationToken) =
lock gate (fun () ->
if ct.IsCancellationRequested then
GraphNode(node.Return(None, [||]))
GraphNode.FromResult(None, [||])
else
let getBuilderNode = GraphNode(CreateOneIncrementalBuilder(options, userOpName))
incrementalBuildersCache.Set(AnyCallerThread, options, getBuilderNode)
Expand Down Expand Up @@ -636,9 +638,6 @@ type BackgroundCompiler
) =

node {
if useChangeNotifications then
do! builder.NotifyFileChanged(fileName, DateTime.UtcNow)

match! bc.GetCachedCheckFileResult(builder, fileName, sourceText, options) with
| Some (_, results) -> return FSharpCheckFileAnswer.Succeeded results
| _ ->
Expand Down Expand Up @@ -1263,7 +1262,8 @@ type FSharpChecker
parallelReferenceResolution,
captureIdentifiersWhenParsing,
getSource,
useChangeNotifications
useChangeNotifications,
useSyntaxTreeCache
) =

let backgroundCompiler =
Expand All @@ -1280,7 +1280,8 @@ type FSharpChecker
parallelReferenceResolution,
captureIdentifiersWhenParsing,
getSource,
useChangeNotifications
useChangeNotifications,
useSyntaxTreeCache
)

static let globalInstance = lazy FSharpChecker.Create()
Expand Down Expand Up @@ -1324,7 +1325,8 @@ type FSharpChecker
?enablePartialTypeChecking,
?parallelReferenceResolution: bool,
?captureIdentifiersWhenParsing: bool,
?documentSource: DocumentSource
?documentSource: DocumentSource,
?useSyntaxTreeCache: bool
) =

use _ = Activity.startNoTags "FSharpChecker.Create"
Expand Down Expand Up @@ -1352,6 +1354,8 @@ type FSharpChecker
| Some (DocumentSource.Custom _) -> true
| _ -> false

let useSyntaxTreeCache = defaultArg useSyntaxTreeCache true

if keepAssemblyContents && enablePartialTypeChecking then
invalidArg "enablePartialTypeChecking" "'keepAssemblyContents' and 'enablePartialTypeChecking' cannot be both enabled."

Expand All @@ -1372,7 +1376,8 @@ type FSharpChecker
(match documentSource with
| Some (DocumentSource.Custom f) -> Some f
| _ -> None),
useChangeNotifications
useChangeNotifications,
useSyntaxTreeCache
)

member _.ReferenceResolver = legacyReferenceResolver
Expand Down
5 changes: 3 additions & 2 deletions src/Compiler/Service/service.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type public FSharpChecker =
/// <param name="parallelReferenceResolution">Indicates whether to resolve references in parallel.</param>
/// <param name="captureIdentifiersWhenParsing">When set to true we create a set of all identifiers for each parsed file which can be used to speed up finding references.</param>
/// <param name="documentSource">Default: FileSystem. You can use Custom source to provide a function that will return the source for a given file path instead of reading it from the file system. Note that with this option the FSharpChecker will also not monitor the file system for file changes. It will expect to be notified of changes via the NotifyFileChanged method.</param>
/// <param name="useSyntaxTreeCache">Default: true. Indicates whether to keep parsing results in a cache.</param>
static member Create:
?projectCacheSize: int *
?keepAssemblyContents: bool *
Expand All @@ -53,7 +54,8 @@ type public FSharpChecker =
?enablePartialTypeChecking: bool *
?parallelReferenceResolution: bool *
?captureIdentifiersWhenParsing: bool *
[<Experimental "This parameter is experimental and likely to be removed in the future.">] ?documentSource: DocumentSource ->
[<Experimental "This parameter is experimental and likely to be removed in the future.">] ?documentSource: DocumentSource *
[<Experimental "This parameter is experimental and likely to be removed in the future.">] ?useSyntaxTreeCache: bool ->
FSharpChecker

/// <summary>
Expand Down Expand Up @@ -383,7 +385,6 @@ type public FSharpChecker =
member ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients: unit -> unit

/// Notify the checker that given file has changed. This needs to be used when checker is created with documentSource = Custom.
/// Although it is not mandatory when the changed file is the next thing requested to be checked.
[<Experimental "This FCS API is experimental and likely to be removed in the future.">]
member NotifyFileChanged: fileName: string * options: FSharpProjectOptions * ?userOpName: string -> Async<unit>

Expand Down
11 changes: 10 additions & 1 deletion src/Compiler/Utilities/Activity.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ open System.Text
[<RequireQualifiedAccess>]
module internal Activity =

let FscSourceName = "fsc"

module Tags =
let fileName = "fileName"
let project = "project"
Expand Down Expand Up @@ -40,7 +42,10 @@ module internal Activity =
outputDllFile
|]

let private activitySourceName = "fsc"
module Events =
let cacheHit = "cacheHit"

let private activitySourceName = FscSourceName
let private profiledSourceName = "fsc_with_env_stats"

type System.Diagnostics.Activity with
Expand Down Expand Up @@ -75,6 +80,10 @@ module internal Activity =

let startNoTags (name: string) : IDisposable = activitySource.StartActivity(name)

let addEvent name =
if Activity.Current <> null && Activity.Current.Source = activitySource then
Activity.Current.AddEvent(ActivityEvent(name)) |> ignore

module Profiling =

module Tags =
Expand Down
7 changes: 7 additions & 0 deletions src/Compiler/Utilities/Activity.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ open System
[<RequireQualifiedAccess>]
module internal Activity =

val FscSourceName: string

module Tags =
val fileName: string
val qualifiedNameOfFile: string
Expand All @@ -17,10 +19,15 @@ module internal Activity =
val length: string
val cache: string

module Events =
val cacheHit: string

val startNoTags: name: string -> IDisposable

val start: name: string -> tags: (string * string) seq -> IDisposable

val addEvent: name: string -> unit

module Profiling =
val startAndMeasureEnvironmentStats: name: string -> IDisposable
val addConsoleListener: unit -> IDisposable
Expand Down
40 changes: 40 additions & 0 deletions src/FSharp.Core/array.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,46 @@ module Array =
module Parallel =
open System.Threading.Tasks

[<CompiledName("TryFindIndex")>]
let tryFindIndex predicate (array: _[]) =
checkNonNull "array" array

let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
if predicate array[i] then
pState.Break())
)

pResult.LowestBreakIteration |> Option.ofNullable |> Option.map int

[<CompiledName("TryFind")>]
let tryFind predicate (array: _[]) =
array |> tryFindIndex predicate |> Option.map (fun i -> array[i])

[<CompiledName("TryPick")>]
let tryPick chooser (array: _[]) =
checkNonNull "array" array
let allChosen = new System.Collections.Concurrent.ConcurrentDictionary<_, _>()

let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
match chooser array[i] with
| None -> ()
| chosenElement ->
allChosen[i] <- chosenElement
pState.Break())
)

pResult.LowestBreakIteration
|> Option.ofNullable
|> Option.bind (fun i -> allChosen[int i])

[<CompiledName("Choose")>]
let choose chooser (array: 'T[]) =
checkNonNull "array" array
Expand Down
96 changes: 96 additions & 0 deletions src/FSharp.Core/array.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -3094,6 +3094,102 @@ module Array =
/// <summary>Provides parallel operations on arrays </summary>
module Parallel =

/// <summary>Returns the first element for which the given function returns <c>True</c>.
/// Returns None if no such element exists.</summary>
///
/// <param name="predicate">The function to test the input elements.</param>
/// <param name="array">The input array.</param>
///
/// <returns>The first element that satisfies the predicate, or None.</returns>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <example id="para-tryfind-1">Try to find the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 2; 3 |]
///
/// inputs |> Array.Parallel.tryFind (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>Some 2</c>.
/// </example>
///
/// <example id="para-tryfind-2">Try to find the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 5; 3 |]
///
/// inputs |> Array.Parallel.tryFind (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>None</c>
/// </example>
[<CompiledName("TryFind")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryFind: predicate:('T -> bool) -> array:'T[] -> 'T option


/// <summary>Returns the index of the first element in the array
/// that satisfies the given predicate.
/// Returns <c>None</c> if no such element exists.</summary>
/// <param name="predicate">The function to test the input elements.</param>
/// <param name="array">The input array.</param>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <returns>The index of the first element that satisfies the predicate, or None.</returns>
///
/// <example id="para-tryfindindex-1">Try to find the index of the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 2; 3; 4; 5 |]
///
/// inputs |> Array.Parallel.tryFindIndex (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>Some 1</c>
/// </example>
///
/// <example id="para-tryfindindex-2">Try to find the index of the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 3; 5; 7 |]
///
/// inputs |> Array.Parallel.tryFindIndex (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>None</c>
/// </example>
[<CompiledName("TryFindIndex")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryFindIndex : predicate:('T -> bool) -> array:'T[] -> int option

/// <summary>Applies the given function to successive elements, returning the first
/// result where the function returns <c>Some(x)</c> for some <c>x</c>. If the function
/// never returns <c>Some(x)</c> then <c>None</c> is returned.</summary>
///
/// <param name="chooser">The function to transform the array elements into options.</param>
/// <param name="array">The input array.</param>
///
/// <returns>The first transformed element that is <c>Some(x)</c>.</returns>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <example id="para-trypick-1">
/// <code lang="fsharp">
/// let input = [| 1; 2; 3 |]
///
/// input |> Array.Parallel.tryPick (fun n -> if n % 2 = 0 then Some (string n) else None)
/// </code>
/// Evaluates to <c>Some 2</c>.
/// </example>
///
/// <example id="para-trypick-2">
/// <code lang="fsharp">
/// let input = [| 1; 2; 3 |]
///
/// input |> Array.Parallel.tryPick (fun n -> if n > 3 = 0 then Some (string n) else None)
/// </code>
/// Evaluates to <c>None</c>.
/// </example>
///
[<CompiledName("TryPick")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryPick: chooser:('T -> 'U option) -> array:'T[] -> 'U option

/// <summary>Apply the given function to each element of the array. Return
/// the array comprised of the results <c>x</c> for each element where
/// the function returns <c>Some(x)</c>.</summary>
Expand Down
Loading