Skip to content

Commit 1e681a6

Browse files
committed
Use a Confinement Queue to Protect IncrementalCompilationState
This class is serendipitously used only from the main queue for the Driver's usual use cases. But we have no reason to expect clients of libSwiftDriver to manipulate this class in the same manner. Provide a serial confinement queue and protect the mutable members of IncrementalCompilationState with it. In the future, we ought to drop the queue and use actors. rdar://75744005
1 parent cdba7d1 commit 1e681a6

File tree

1 file changed

+68
-34
lines changed

1 file changed

+68
-34
lines changed

Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,25 @@ import TSCBasic
1313
import Foundation
1414
import SwiftOptions
1515

16-
public class IncrementalCompilationState {
16+
/// An instance of `IncrementalCompilationState` encapsulates the data necessary
17+
/// to make incremental builds scheduling decisions.
18+
///
19+
/// The primary form of interaction with the incremental compilation state is
20+
/// using it as an oracle to discover the jobs to execute as the incremental
21+
/// build progresses. After a job completes, call
22+
/// `IncrementalCompilationState.collectJobsDiscoveredToBeNeededAfterFinishing(job:)`
23+
/// to both update the incremental state and recieve an array of jobs that
24+
/// need to be executed in response.
25+
///
26+
/// Jobs become "unstuck" as their inputs become available, or may be discovered
27+
/// by this class as fresh dependency information is integrated.
28+
///
29+
/// Threading Considerations
30+
/// ========================
31+
///
32+
/// The public API surface of this class is thread safe, but not re-entrant.
33+
/// FIXME: This should be an actor.
34+
public final class IncrementalCompilationState {
1735

1836
/// The oracle for deciding what depends on what. Applies to this whole module.
1937
@_spi(Testing) public let moduleDependencyGraph: ModuleDependencyGraph
@@ -25,15 +43,24 @@ public class IncrementalCompilationState {
2543
/// Already batched, and in order of input files.
2644
public let mandatoryJobsInOrder: [Job]
2745

46+
/// Jobs to run *after* the last compile, for instance, link-editing.
47+
public let jobsAfterCompiles: [Job]
48+
49+
/// A high-priority confinement queue that must be used to protect the incremental compilation state.
50+
private let confinementQueue: DispatchQueue = DispatchQueue(label: "com.apple.swift-driver.incremental-compilation-state", qos: .userInteractive)
51+
2852
/// Sadly, has to be `var` for formBatchedJobs
53+
///
54+
/// After initialization, mutating accesses to the driver must be protected by
55+
/// the confinement queue.
2956
private var driver: Driver
3057

3158
/// Keyed by primary input. As required compilations are discovered after the first wave, these shrink.
59+
///
60+
/// This state is modified during the incremental build. All accesses must
61+
/// be protected by the confinement queue.
3262
private var skippedCompileGroups = [TypedVirtualPath: CompileJobGroup]()
3363

34-
/// Jobs to run *after* the last compile, for instance, link-editing.
35-
public let jobsAfterCompiles: [Job]
36-
3764
// MARK: - Creating IncrementalCompilationState if possible
3865
/// Return nil if not compiling incrementally
3966
init?(
@@ -124,10 +151,6 @@ extension IncrementalCompilationState {
124151
/// for the first wave to execute.
125152
let mandatoryJobsInOrder: [Job]
126153
}
127-
128-
129-
130-
131154
}
132155

133156
extension Driver {
@@ -203,7 +226,7 @@ extension IncrementalCompilationState {
203226
let invalidatedInputs = collectInputsInvalidatedByRunning(finishedJob)
204227
assert(Set(invalidatedInputs).isDisjoint(with: finishedJob.primaryInputs),
205228
"Primaries should not overlap secondaries.")
206-
229+
207230
if let reporter = self.reporter {
208231
for input in invalidatedInputs {
209232
reporter.report(
@@ -222,17 +245,21 @@ extension IncrementalCompilationState {
222245

223246
/// After `job` finished find out which inputs must compiled that were not known to need compilation before
224247
private func collectInputsInvalidatedByRunning(_ job: Job)-> Set<TypedVirtualPath> {
225-
guard job.kind == .compile else {
226-
return Set<TypedVirtualPath>()
227-
}
228-
return job.primaryInputs.reduce(into: Set()) { invalidatedInputs, primaryInput in
229-
invalidatedInputs.formUnion(collectInputsInvalidated(byCompiling: primaryInput))
248+
return self.confinementQueue.sync {
249+
guard job.kind == .compile else {
250+
return Set<TypedVirtualPath>()
251+
}
252+
return job.primaryInputs.reduce(into: Set()) { invalidatedInputs, primaryInput in
253+
invalidatedInputs.formUnion(collectInputsInvalidated(byCompiling: primaryInput))
254+
}
255+
.subtracting(job.primaryInputs) // have already compiled these
230256
}
231-
.subtracting(job.primaryInputs) // have already compiled these
232257
}
233258

234-
private func collectInputsInvalidated(byCompiling input: TypedVirtualPath)
235-
-> TransitivelyInvalidatedInputSet {
259+
private func collectInputsInvalidated(
260+
byCompiling input: TypedVirtualPath
261+
) -> TransitivelyInvalidatedInputSet {
262+
dispatchPrecondition(condition: .onQueue(self.confinementQueue))
236263
if let found = moduleDependencyGraph.collectInputsRequiringCompilation(byCompiling: input) {
237264
return found
238265
}
@@ -242,34 +269,41 @@ extension IncrementalCompilationState {
242269
}
243270

244271
/// Find the jobs that now must be run that were not originally known to be needed.
245-
private func getJobs(for invalidatedInputs: Set<TypedVirtualPath>
272+
private func getJobs(
273+
for invalidatedInputs: Set<TypedVirtualPath>
246274
) throws -> [Job] {
247-
let unbatched = invalidatedInputs.flatMap { input -> [Job] in
248-
if let group = skippedCompileGroups.removeValue(forKey: input) {
249-
let primaryInputs = group.compileJob.primaryInputs
250-
assert(primaryInputs.count == 1)
251-
assert(primaryInputs[0] == input)
252-
self.reporter?.report("Scheduling invalidated", input)
253-
return group.allJobs()
254-
}
255-
else {
256-
self.reporter?.report("Tried to schedule invalidated input again", input)
257-
return []
275+
return try self.confinementQueue.sync {
276+
let unbatched = invalidatedInputs.flatMap { input -> [Job] in
277+
if let group = skippedCompileGroups.removeValue(forKey: input) {
278+
let primaryInputs = group.compileJob.primaryInputs
279+
assert(primaryInputs.count == 1)
280+
assert(primaryInputs[0] == input)
281+
self.reporter?.report("Scheduling invalidated", input)
282+
return group.allJobs()
283+
}
284+
else {
285+
self.reporter?.report("Tried to schedule invalidated input again", input)
286+
return []
287+
}
258288
}
289+
return try driver.formBatchedJobs(unbatched, showJobLifecycle: driver.showJobLifecycle)
259290
}
260-
return try driver.formBatchedJobs(unbatched, showJobLifecycle: driver.showJobLifecycle)
261291
}
262292
}
263293

264294
// MARK: - After the build
265295
extension IncrementalCompilationState {
266296
var skippedCompilationInputs: Set<TypedVirtualPath> {
267-
Set(skippedCompileGroups.keys)
297+
return self.confinementQueue.sync {
298+
Set(skippedCompileGroups.keys)
299+
}
268300
}
269301
public var skippedJobs: [Job] {
270-
skippedCompileGroups.values
271-
.sorted {$0.primaryInput.file.name < $1.primaryInput.file.name}
272-
.flatMap {$0.allJobs()}
302+
return self.confinementQueue.sync {
303+
skippedCompileGroups.values
304+
.sorted {$0.primaryInput.file.name < $1.primaryInput.file.name}
305+
.flatMap {$0.allJobs()}
306+
}
273307
}
274308
}
275309

0 commit comments

Comments
 (0)