Skip to content

Commit d632aa4

Browse files
authored
Improve performance of graph-based type-checking - don't validate filepaths unnecessarily (#15431)
* Avoid checking the same filenames for every graph node. * Speedup `queryTrie` - don't memoize as that's costly, refactor the code
1 parent 9c55d32 commit d632aa4

File tree

3 files changed

+72
-61
lines changed

3 files changed

+72
-61
lines changed

src/Compiler/Driver/GraphChecking/DependencyResolution.fs

Lines changed: 70 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,45 @@
11
module internal FSharp.Compiler.GraphChecking.DependencyResolution
22

3-
open FSharp.Compiler.IO
43
open FSharp.Compiler.Syntax
4+
open Internal.Utilities.Library
55

6-
/// <summary>Find a path in the Trie.</summary>
7-
/// <remarks>This function could be cached in future if performance is an issue.</remarks>
8-
let queryTrie (trie: TrieNode) (path: LongIdentifier) : QueryTrieNodeResult =
6+
/// <summary>Find a path from a starting TrieNode and return the end node or None</summary>
7+
let queryTriePartial (trie: TrieNode) (path: LongIdentifier) : TrieNode option =
98
let rec visit (currentNode: TrieNode) (path: LongIdentifier) =
109
match path with
11-
| [] -> failwith "path should not be empty"
12-
| [ lastNodeFromPath ] ->
13-
match currentNode.Children.TryGetValue(lastNodeFromPath) with
14-
| false, _ -> QueryTrieNodeResult.NodeDoesNotExist
15-
| true, childNode ->
16-
if Set.isEmpty childNode.Files then
17-
QueryTrieNodeResult.NodeDoesNotExposeData
18-
else
19-
QueryTrieNodeResult.NodeExposesData(childNode.Files)
10+
// When we get through all partial identifiers, we've reached the node the full path points to.
11+
| [] -> Some currentNode
12+
// More segments to get through
2013
| currentPath :: restPath ->
2114
match currentNode.Children.TryGetValue(currentPath) with
22-
| false, _ -> QueryTrieNodeResult.NodeDoesNotExist
15+
| false, _ -> None
2316
| true, childNode -> visit childNode restPath
2417

2518
visit trie path
2619

27-
let queryTrieMemoized (trie: TrieNode) : QueryTrie =
28-
Internal.Utilities.Library.Tables.memoize (queryTrie trie)
20+
let mapNodeToQueryResult (node: TrieNode option) : QueryTrieNodeResult =
21+
match node with
22+
| Some finalNode ->
23+
if Set.isEmpty finalNode.Files then
24+
QueryTrieNodeResult.NodeDoesNotExposeData
25+
else
26+
QueryTrieNodeResult.NodeExposesData(finalNode.Files)
27+
| None -> QueryTrieNodeResult.NodeDoesNotExist
28+
29+
/// <summary>Find a path in the Trie.</summary>
30+
let queryTrie (trie: TrieNode) (path: LongIdentifier) : QueryTrieNodeResult =
31+
queryTriePartial trie path |> mapNodeToQueryResult
32+
33+
/// <summary>Same as 'queryTrie' but allows passing in a path combined from two parts, avoiding list allocation.</summary>
34+
let queryTrieDual (trie: TrieNode) (path1: LongIdentifier) (path2: LongIdentifier) : QueryTrieNodeResult =
35+
match queryTriePartial trie path1 with
36+
| Some intermediateNode -> queryTriePartial intermediateNode path2
37+
| None -> None
38+
|> mapNodeToQueryResult
2939

3040
/// Process namespace declaration.
31-
let processNamespaceDeclaration (queryTrie: QueryTrie) (path: LongIdentifier) (state: FileContentQueryState) : FileContentQueryState =
32-
let queryResult = queryTrie path
41+
let processNamespaceDeclaration (trie: TrieNode) (path: LongIdentifier) (state: FileContentQueryState) : FileContentQueryState =
42+
let queryResult = queryTrie trie path
3343

3444
match queryResult with
3545
| QueryTrieNodeResult.NodeDoesNotExist -> state
@@ -38,18 +48,16 @@ let processNamespaceDeclaration (queryTrie: QueryTrie) (path: LongIdentifier) (s
3848

3949
/// Process an "open" statement.
4050
/// The statement could link to files and/or should be tracked as an open namespace.
41-
let processOpenPath (queryTrie: QueryTrie) (path: LongIdentifier) (state: FileContentQueryState) : FileContentQueryState =
42-
let queryResult = queryTrie path
51+
let processOpenPath (trie: TrieNode) (path: LongIdentifier) (state: FileContentQueryState) : FileContentQueryState =
52+
let queryResult = queryTrie trie path
4353

4454
match queryResult with
4555
| QueryTrieNodeResult.NodeDoesNotExist -> state
4656
| QueryTrieNodeResult.NodeDoesNotExposeData -> state.AddOpenNamespace path
4757
| QueryTrieNodeResult.NodeExposesData files -> state.AddOpenNamespace(path, files)
4858

4959
/// Process an identifier.
50-
let processIdentifier (queryTrie: QueryTrie) (path: LongIdentifier) (state: FileContentQueryState) : FileContentQueryState =
51-
let queryResult = queryTrie path
52-
60+
let processIdentifier (queryResult: QueryTrieNodeResult) (state: FileContentQueryState) : FileContentQueryState =
5361
match queryResult with
5462
| QueryTrieNodeResult.NodeDoesNotExist -> state
5563
| QueryTrieNodeResult.NodeDoesNotExposeData ->
@@ -59,26 +67,26 @@ let processIdentifier (queryTrie: QueryTrie) (path: LongIdentifier) (state: File
5967
| QueryTrieNodeResult.NodeExposesData files -> state.AddDependencies files
6068

6169
/// Typically used to fold FileContentEntry items over a FileContentQueryState
62-
let rec processStateEntry (queryTrie: QueryTrie) (state: FileContentQueryState) (entry: FileContentEntry) : FileContentQueryState =
70+
let rec processStateEntry (trie: TrieNode) (state: FileContentQueryState) (entry: FileContentEntry) : FileContentQueryState =
6371
match entry with
6472
| FileContentEntry.TopLevelNamespace (topLevelPath, content) ->
6573
let state =
6674
match topLevelPath with
6775
| [] -> state
68-
| _ -> processNamespaceDeclaration queryTrie topLevelPath state
76+
| _ -> processNamespaceDeclaration trie topLevelPath state
6977

70-
List.fold (processStateEntry queryTrie) state content
78+
List.fold (processStateEntry trie) state content
7179

7280
| FileContentEntry.OpenStatement path ->
7381
// An open statement can directly reference file or be a partial open statement
7482
// Both cases need to be processed.
75-
let stateAfterFullOpenPath = processOpenPath queryTrie path state
83+
let stateAfterFullOpenPath = processOpenPath trie path state
7684

77-
// Any existing open statement could be extended with the current path (if that node where to exists in the trie)
85+
// Any existing open statement could be extended with the current path (if that node were to exists in the trie)
7886
// The extended path could add a new link (in case of a module or namespace with types)
79-
// It might also not add anything at all (in case it the extended path is still a partial one)
87+
// It might also not add anything at all (in case the extended path is still a partial one)
8088
(stateAfterFullOpenPath, state.OpenNamespaces)
81-
||> Set.fold (fun acc openNS -> processOpenPath queryTrie [ yield! openNS; yield! path ] acc)
89+
||> Set.fold (fun acc openNS -> processOpenPath trie [ yield! openNS; yield! path ] acc)
8290

8391
| FileContentEntry.PrefixedIdentifier path ->
8492
match path with
@@ -91,15 +99,17 @@ let rec processStateEntry (queryTrie: QueryTrie) (state: FileContentQueryState)
9199
||> Array.fold (fun state takeParts ->
92100
let path = List.take takeParts path
93101
// process the name was if it were a FQN
94-
let stateAfterFullIdentifier = processIdentifier queryTrie path state
102+
let stateAfterFullIdentifier = processIdentifier (queryTrieDual trie [] path) state
95103

96104
// Process the name in combination with the existing open namespaces
97105
(stateAfterFullIdentifier, state.OpenNamespaces)
98-
||> Set.fold (fun acc openNS -> processIdentifier queryTrie [ yield! openNS; yield! path ] acc))
106+
||> Set.fold (fun acc openNS ->
107+
let queryResult = queryTrieDual trie openNS path
108+
processIdentifier queryResult acc))
99109

100110
| FileContentEntry.NestedModule (nestedContent = nestedContent) ->
101111
// We don't want our current state to be affect by any open statements in the nested module
102-
let nestedState = List.fold (processStateEntry queryTrie) state nestedContent
112+
let nestedState = List.fold (processStateEntry trie) state nestedContent
103113
// Afterward we are only interested in the found dependencies in the nested module
104114
let foundDependencies =
105115
Set.union state.FoundDependencies nestedState.FoundDependencies
@@ -123,11 +133,11 @@ let rec processStateEntry (queryTrie: QueryTrie) (state: FileContentQueryState)
123133
/// This function returns an array with a potential extra dependencies that makes sure that any such namespaces can be resolved (if they exists).
124134
/// For each unused namespace `open` we return at most one file that defines that namespace.</para>
125135
/// </remarks>
126-
let collectGhostDependencies (fileIndex: FileIndex) (trie: TrieNode) (queryTrie: QueryTrie) (result: FileContentQueryState) =
136+
let collectGhostDependencies (fileIndex: FileIndex) (trie: TrieNode) (result: FileContentQueryState) =
127137
// For each opened namespace, if none of already resolved dependencies define it, return the top-most file that defines it.
128138
Set.toArray result.OpenedNamespaces
129139
|> Array.choose (fun path ->
130-
match queryTrie path with
140+
match queryTrie trie path with
131141
| QueryTrieNodeResult.NodeExposesData _
132142
| QueryTrieNodeResult.NodeDoesNotExist -> None
133143
| QueryTrieNodeResult.NodeDoesNotExposeData ->
@@ -170,7 +180,6 @@ let mkGraph (compilingFSharpCore: bool) (filePairs: FilePairMap) (files: FileInP
170180
| ParsedInput.SigFile _ -> Some f)
171181

172182
let trie = TrieMapping.mkTrie trieInput
173-
let queryTrie: QueryTrie = queryTrieMemoized trie
174183

175184
let fileContents =
176185
files
@@ -180,14 +189,31 @@ let mkGraph (compilingFSharpCore: bool) (filePairs: FilePairMap) (files: FileInP
180189
else
181190
FileContentMapping.mkFileContent file)
182191

192+
// Files in FSharp.Core have an implicit dependency on `prim-types-prelude.fsi` - add it.
193+
let fsharpCoreImplicitDependency =
194+
if compilingFSharpCore then
195+
let filename = "prim-types-prelude.fsi"
196+
197+
let implicitDepIdx =
198+
files
199+
|> Array.tryFindIndex (fun f -> System.IO.Path.GetFileName(f.FileName) = filename)
200+
201+
match implicitDepIdx with
202+
| Some idx -> Some idx
203+
| None ->
204+
exn $"Expected to find file '{filename}' during compilation of FSharp.Core, but it was not found."
205+
|> raise
206+
else
207+
None
208+
183209
let findDependencies (file: FileInProject) : FileIndex array =
184210
if file.Idx = 0 then
185211
// First file cannot have any dependencies.
186212
Array.empty
187213
else
188214
let fileContent = fileContents[file.Idx]
189215

190-
let knownFiles = [ 0 .. (file.Idx - 1) ] |> set
216+
let knownFiles = [| 0 .. (file.Idx - 1) |] |> set
191217
// File depends on all files above it that define accessible symbols at the root level (global namespace).
192218
let filesFromRoot = trie.Files |> Set.filter (fun rootIdx -> rootIdx < file.Idx)
193219
// Start by listing root-level dependencies.
@@ -197,42 +223,28 @@ let mkGraph (compilingFSharpCore: bool) (filePairs: FilePairMap) (files: FileInP
197223
let depsResult =
198224
initialDepsResult
199225
// Seq is faster than List in this case.
200-
||> Seq.fold (processStateEntry queryTrie)
226+
||> Seq.fold (processStateEntry trie)
201227

202228
// Add missing links for cases where an unused open namespace did not create a link.
203-
let ghostDependencies = collectGhostDependencies file.Idx trie queryTrie depsResult
229+
let ghostDependencies = collectGhostDependencies file.Idx trie depsResult
204230

205231
// Add a link from implementation files to their signature files.
206232
let signatureDependency =
207233
match filePairs.TryGetSignatureIndex file.Idx with
208234
| None -> Array.empty
209235
| Some sigIdx -> Array.singleton sigIdx
210236

211-
// Files in FSharp.Core have an implicit dependency on `prim-types-prelude.fsi` - add it.
212-
let fsharpCoreImplicitDependencies =
213-
let filename = "prim-types-prelude.fsi"
214-
215-
let implicitDepIdx =
216-
files
217-
|> Array.tryFindIndex (fun f -> FileSystemUtils.fileNameOfPath f.FileName = filename)
218-
219-
[|
220-
if compilingFSharpCore then
221-
match implicitDepIdx with
222-
| Some idx ->
223-
if file.Idx > idx then
224-
yield idx
225-
| None ->
226-
exn $"Expected to find file '{filename}' during compilation of FSharp.Core, but it was not found."
227-
|> raise
228-
|]
237+
let fsharpCoreImplicitDependencyForThisFile =
238+
match fsharpCoreImplicitDependency with
239+
| Some depIdx when file.Idx > depIdx -> [| depIdx |]
240+
| _ -> [||]
229241

230242
let allDependencies =
231243
[|
232244
yield! depsResult.FoundDependencies
233245
yield! ghostDependencies
234246
yield! signatureDependency
235-
yield! fsharpCoreImplicitDependencies
247+
yield! fsharpCoreImplicitDependencyForThisFile
236248
|]
237249
|> Array.distinct
238250

src/Compiler/Driver/GraphChecking/DependencyResolution.fsi

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ val queryTrie: trie: TrieNode -> path: LongIdentifier -> QueryTrieNodeResult
77

88
/// <summary>Process an open path (found in the ParsedInput) with a given FileContentQueryState.</summary>
99
/// <remarks>This code is only used directly in unit tests.</remarks>
10-
val processOpenPath:
11-
queryTrie: QueryTrie -> path: LongIdentifier -> state: FileContentQueryState -> FileContentQueryState
10+
val processOpenPath: trie: TrieNode -> path: LongIdentifier -> state: FileContentQueryState -> FileContentQueryState
1211

1312
/// <summary>
1413
/// Construct an approximate* dependency graph for files within a project, based on their ASTs.

tests/FSharp.Compiler.ComponentTests/TypeChecks/Graph/QueryTrieTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ let ``ProcessOpenStatement full path match`` () =
807807
Set.empty
808808

809809
let result =
810-
processOpenPath (queryTrie fantomasCoreTrie) [ "Fantomas"; "Core"; "AstExtensions" ] state
810+
processOpenPath fantomasCoreTrie [ "Fantomas"; "Core"; "AstExtensions" ] state
811811

812812
let dep = Seq.exactlyOne result.FoundDependencies
813813
Assert.AreEqual(indexOf "AstExtensions.fsi", dep)

0 commit comments

Comments
 (0)