Skip to content

Commit 1f9384c

Browse files
committed
Cleanup
1 parent a5a2bd1 commit 1f9384c

File tree

4 files changed

+92
-78
lines changed

4 files changed

+92
-78
lines changed
Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
/// Parallel processing of graph of work items with dependencies
22
module ParallelTypeCheckingTests.GraphProcessing
33

4-
open System.Collections.Concurrent
5-
open System.Collections.Generic
64
open System.Threading
7-
open ParallelTypeCheckingTests.Parallel
85

9-
/// Used for processing
106
type NodeInfo<'Item> =
117
{
128
Item: 'Item
@@ -15,38 +11,42 @@ type NodeInfo<'Item> =
1511
Dependants: 'Item[]
1612
}
1713

18-
// TODO Do not expose this type to other files
19-
type Node<'Item, 'Result> =
14+
type private PrivateNode<'Item, 'Result> =
2015
{
2116
Info: NodeInfo<'Item>
2217
mutable ProcessedDepsCount: int
2318
mutable Result: 'Result option
2419
}
25-
26-
/// Basic concurrent set implemented using ConcurrentDictionary
27-
type private ConcurrentSet<'a>() =
28-
let dict = ConcurrentDictionary<'a, unit>()
29-
30-
member this.Add(item: 'a): bool =
31-
dict.TryAdd(item, ())
3220

21+
type ProcessedNode<'Item, 'Result> =
22+
{
23+
Info: NodeInfo<'Item>
24+
Result: 'Result
25+
}
26+
3327
/// <summary>
3428
/// A generic method to generate results for a graph of work items in parallel.
3529
/// Processes leaves first, and after each node has been processed, schedules any now unblocked dependants.
3630
/// Returns a list of results, per item.
31+
/// Uses ThreadPool to schedule work.
3732
/// </summary>
3833
/// <param name="graph">Graph of work items</param>
39-
/// <param name="doWork">A function to generate results for a single item</param>
40-
let processGraphSimple<'Item, 'Result when 'Item: equality and 'Item: comparison>
34+
/// <param name="work">A function to generate results for a single item</param>
35+
/// <param name="ct">Cancellation token</param>
36+
/// <remarks>
37+
/// An alternative scheduling approach is to schedule N parallel tasks that process items from a BlockingCollection.
38+
/// My basic tests suggested it's faster, although confirming that would require more detailed testing.
39+
/// </remarks>
40+
let processGraph<'Item, 'Result when 'Item: equality and 'Item: comparison>
4141
(graph: Graph<'Item>)
42-
// TODO Avoid exposing mutable nodes to the caller
43-
(doWork: IReadOnlyDictionary<'Item, Node<'Item, 'Result>> -> Node<'Item, 'Result> -> 'Result)
44-
: 'Result[] // Results in order defined in 'graph'
42+
(work: ('Item -> ProcessedNode<'Item, 'Result>) -> NodeInfo<'Item> -> 'Result)
43+
(ct: CancellationToken)
44+
: ('Item * 'Result)[] // Individual item results
4545
=
4646
let transitiveDeps = graph |> Graph.transitiveOpt
4747
let dependants = graph |> Graph.reverse
4848

49-
let makeNode (item: 'Item) : Node<'Item, 'Result> =
49+
let makeNode (item: 'Item) : PrivateNode<'Item, 'Result> =
5050
let info =
5151
let exists = graph.ContainsKey item
5252

@@ -80,40 +80,59 @@ let processGraphSimple<'Item, 'Result when 'Item: equality and 'Item: comparison
8080
|> Seq.filter (fun n -> n.Info.Deps.Length = 0)
8181
|> Seq.toArray
8282

83-
printfn $"Node count: {nodes.Count}"
84-
use cts = new CancellationTokenSource()
85-
86-
let mutable processedCount = 0
8783
let waitHandle = new AutoResetEvent(false)
88-
let rec post node =
89-
Async.Start(async {work node}, cts.Token)
90-
and work
91-
(node: Node<'Item, 'Result>)
84+
85+
let getItemPublicNode item =
86+
let node = nodes[item]
87+
{
88+
ProcessedNode.Info = node.Info
89+
ProcessedNode.Result =
90+
node.Result
91+
|> Option.defaultWith (fun () -> failwith $"Results for item '{node.Info.Item}' are not yet available")
92+
}
93+
94+
let incrementProcessedCount =
95+
let mutable processedCount = 0
96+
fun () ->
97+
if Interlocked.Increment(&processedCount) = nodes.Count then
98+
waitHandle.Set() |> ignore
99+
100+
let rec queueNode node =
101+
Async.Start(async {processNode node}, ct)
102+
103+
and processNode
104+
(node: PrivateNode<'Item, 'Result>)
92105
: unit =
93-
let singleRes = doWork nodes node
106+
let info = node.Info
107+
108+
let singleRes = work getItemPublicNode info
94109
node.Result <- Some singleRes
110+
95111
let unblockedDependants =
96112
node.Info.Dependants
97113
|> lookupMany
98114
// For every dependant, increment its number of processed dependencies,
99-
// and filter ones which now have all dependencies processed.
115+
// and filter dependants which now have all dependencies processed (but didn't before).
100116
|> Array.filter (fun dependant ->
101117
// This counter can be incremented by multiple workers on different threads.
102118
let pdc = Interlocked.Increment(&dependant.ProcessedDepsCount)
103119
// Note: We cannot read 'dependant.ProcessedDepsCount' again to avoid returning the same item multiple times.
104120
pdc = dependant.Info.Deps.Length)
105-
unblockedDependants |> Array.iter post
106-
if Interlocked.Increment(&processedCount) = nodes.Count then
107-
waitHandle.Set() |> ignore
121+
122+
unblockedDependants |> Array.iter queueNode
123+
incrementProcessedCount()
108124

109-
leaves |> Array.iter post
125+
leaves |> Array.iter queueNode
110126
// TODO Handle async exceptions
111127
// q.Error += ...
112128
waitHandle.WaitOne() |> ignore
113129

114130
nodes.Values
115131
|> Seq.map (fun node ->
116-
node.Result
117-
|> Option.defaultWith (fun () -> failwith $"Unexpected lack of result for item '{node.Info.Item}'")
132+
let result =
133+
node.Result
134+
|> Option.defaultWith (fun () -> failwith $"Unexpected lack of result for item '{node.Info.Item}'")
135+
node.Info.Item, result
118136
)
119-
|> Seq.toArray
137+
|> Seq.sortBy fst
138+
|> Seq.toArray

tests/ParallelTypeCheckingTests/Code/Parallel.fs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
module ParallelTypeCheckingTests.Parallel
22

3-
#nowarn "1182"
4-
53
open System
64
open System.Collections.Concurrent
75
open System.Threading
86

9-
// TODO Could replace with MailboxProcessor+Tasks/Asyncs instead of BlockingCollection + Threads
10-
// See http://www.fssnip.net/nX/title/Limit-degree-of-parallelism-using-an-agent
117
/// Process items in parallel, allow more work to be scheduled as a result of finished work,
128
/// limit parallelisation to 'parallelism' threads
139
let processInParallel
@@ -24,18 +20,13 @@ let processInParallel
2420
let mutable processedCount = 0
2521

2622
let processItem item =
27-
// printfn $"Processing {_itemToString item}"
2823
let toSchedule = work item
2924

3025
let processedCount =
3126
lock processedCountLock (fun () ->
3227
processedCount <- processedCount + 1
3328
processedCount)
3429

35-
// let toScheduleString =
36-
// toSchedule |> Array.map _itemToString |> (fun names -> String.Join(", ", names))
37-
38-
// printfn $"Scheduling {toSchedule.Length} items: {toScheduleString}"
3930
toSchedule |> Array.iter bc.Add
4031
processedCount
4132

tests/ParallelTypeCheckingTests/Code/ParallelTypeChecking.fs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ let folder (isFinalFold: bool) (state: State) (result: SingleResult) : FinalFile
4242
let CheckMultipleInputsInParallel
4343
((ctok, checkForErrors, tcConfig: TcConfig, tcImports: TcImports, tcGlobals, prefixPathOpt, tcState, eagerFormat, inputs): 'a * (unit -> bool) * TcConfig * TcImports * TcGlobals * LongIdent option * TcState * (PhasedDiagnostic -> PhasedDiagnostic) * ParsedInput list)
4444
: FinalFileResult list * TcState =
45+
46+
use cts = new CancellationTokenSource()
4547

4648
let sourceFiles: FileWithAST array =
4749
inputs
@@ -140,11 +142,17 @@ let CheckMultipleInputsInParallel
140142
let state: State = tcState, priorErrors
141143

142144
let partialResults, (tcState, _) =
143-
TypeCheckingGraphProcessing.processGraph<int, State, SingleResult, FinalFileResult>
145+
TypeCheckingGraphProcessing.processFileGraph<int, State, SingleResult, FinalFileResult>
144146
graph
145147
processFile
146148
folder
147149
state
148-
10
149-
150-
partialResults |> Array.toList, tcState)
150+
cts.Token
151+
152+
let partialResults =
153+
partialResults
154+
|> Array.sortBy fst
155+
|> Array.map snd
156+
|> Array.toList
157+
158+
partialResults, tcState)
Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// Parallel processing of graph of work items with dependencies
22
module ParallelTypeCheckingTests.TypeCheckingGraphProcessing
33

4+
open System.IO
45
open ParallelTypeCheckingTests.GraphProcessing
56
open System.Collections.Generic
67
open System.Threading
@@ -14,31 +15,29 @@ open System.Threading
1415
/// <param name="deps">Direct dependencies of a node</param>
1516
/// <param name="transitiveDeps">Transitive dependencies of a node</param>
1617
/// <param name="folder">A way to fold a single result into existing state</param>
18+
/// <remarks>
19+
/// Similar to 'processFileGraph', this function is generic yet specific to the type-checking process.
20+
/// </remarks>
1721
let private combineResults
1822
(emptyState: 'State)
19-
(deps: Node<'Item, 'State * 'Result>[])
20-
(transitiveDeps: Node<'Item, 'State * 'Result>[])
23+
(deps: ProcessedNode<'Item, 'State * 'Result>[])
24+
(transitiveDeps: ProcessedNode<'Item, 'State * 'Result>[])
2125
(folder: 'State -> 'Result -> 'State)
2226
: 'State =
2327
match deps with
2428
| [||] -> emptyState
2529
| _ ->
2630
let biggestDep =
27-
let sizeMetric (node: Node<_,_>) =
31+
let sizeMetric (node: ProcessedNode<_,_>) =
2832
node.Info.TransitiveDeps.Length
2933
deps
3034
|> Array.maxBy sizeMetric
3135

32-
let orFail value =
33-
value |> Option.defaultWith (fun () -> failwith "Unexpected lack of result")
34-
35-
let firstState = biggestDep.Result |> orFail |> fst
36-
37-
// TODO Potential perf optimisation: Keep transDeps in a HashSet from the start,
38-
// avoiding reconstructing the HashSet here
36+
let firstState = biggestDep.Result |> fst
3937

4038
// Add single-file results of remaining transitive deps one-by-one using folder
41-
// Note: Good to preserve order here so that folding happens in file order
39+
// Note: Ordering is not preserved due to reusing results of the biggest child
40+
// rather than starting with empty state
4241
let included =
4342
let set = HashSet(biggestDep.Info.TransitiveDeps)
4443
set.Add biggestDep.Info.Item |> ignore
@@ -48,44 +47,41 @@ let private combineResults
4847
transitiveDeps
4948
|> Array.filter (fun dep -> included.Contains dep.Info.Item = false)
5049
|> Array.distinctBy (fun dep -> dep.Info.Item)
51-
|> Array.map (fun dep -> dep.Result |> orFail |> snd)
50+
|> Array.map (fun dep -> dep.Result |> snd)
5251

5352
let state = Array.fold folder firstState resultsToAdd
5453
state
5554

56-
let processGraph<'Item, 'State, 'Result, 'FinalFileResult when 'Item: equality and 'Item: comparison>
55+
// TODO This function and its parameters are quite specific to type-checking despite using generic types.
56+
// Perhaps we should make it either more specific and remove type parameters, or more generic.
57+
let processFileGraph<'Item, 'State, 'Result, 'FinalFileResult when 'Item: equality and 'Item: comparison>
5758
(graph: Graph<'Item>)
5859
(doWork: 'Item -> 'State -> 'Result)
5960
(folder: bool -> 'State -> 'Result -> 'FinalFileResult * 'State)
6061
(emptyState: 'State)
61-
(_parallelism: int)
62-
: 'FinalFileResult[] * 'State =
62+
(ct: CancellationToken)
63+
: ('Item * 'FinalFileResult)[] * 'State =
6364

6465
let work
65-
(dict: IReadOnlyDictionary<'Item, Node<'Item, 'State * 'Result>>)
66-
(node: Node<'Item, 'State * 'Result>)
66+
(getFinishedNode: 'Item -> ProcessedNode<'Item, 'State * 'Result>)
67+
(node: NodeInfo<'Item>)
6768
: 'State * 'Result =
6869
let folder x y = folder false x y |> snd
69-
let deps = node.Info.Deps |> Array.map (fun node -> dict[node])
70-
let transitiveDeps = node.Info.TransitiveDeps |> Array.map (fun node -> dict[node])
70+
let deps = node.Deps |> Array.except [|node.Item|] |> Array.map getFinishedNode
71+
let transitiveDeps = node.TransitiveDeps|> Array.except [|node.Item|] |> Array.map getFinishedNode
7172
let inputState = combineResults emptyState deps transitiveDeps folder
72-
let singleRes = doWork node.Info.Item inputState
73+
let singleRes = doWork node.Item inputState
7374
let state = folder inputState singleRes
7475
state, singleRes
7576

76-
use cts = new CancellationTokenSource()
77-
78-
let results =
79-
processGraphSimple
80-
graph
81-
work
77+
let results = processGraph graph work ct
8278

83-
let finals, state: 'FinalFileResult[] * 'State =
79+
let finals, state: ('Item * 'FinalFileResult)[] * 'State =
8480
results
8581
|> Array.fold
86-
(fun (fileResults, state) (_, itemRes) ->
82+
(fun (fileResults, state) (item, (_, itemRes)) ->
8783
let fileResult, state = folder true state itemRes
88-
Array.append fileResults [| fileResult |], state)
84+
Array.append fileResults [| item, fileResult |], state)
8985
([||], emptyState)
9086

9187
finals, state

0 commit comments

Comments
 (0)