Skip to content

Commit 07d23c0

Browse files
author
David Ungar
authored
Merge pull request #351 from davidungar/incremental-11-6
[Incremental] Interleave waves by adding llbuild rules dynamically
2 parents 92bf1e4 + f63f70b commit 07d23c0

File tree

13 files changed

+571
-314
lines changed

13 files changed

+571
-314
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -888,29 +888,12 @@ extension Driver {
888888
allJobs: [Job],
889889
forceResponseFiles: Bool
890890
) throws {
891-
// Create and use the tool execution delegate if one is not provided explicitly.
892-
let executorDelegate = createToolExecutionDelegate()
893-
894-
func execute(jobs: [Job]) throws {
895-
try executor.execute(jobs: jobs,
896-
delegate: executorDelegate,
897-
numParallelJobs: numParallelJobs ?? 1,
898-
forceResponseFiles: forceResponseFiles,
899-
recordedInputModificationDates: recordedInputModificationDates)
900-
}
901-
902-
guard let incrementalCompilationState = incrementalCompilationState else {
903-
try execute(jobs: allJobs)
904-
return
905-
}
906-
while let jobs = incrementalCompilationState.preOrCompileJobs.dequeue() {
907-
try execute(jobs: formBatchedJobs(jobs, forIncremental: true))
908-
}
909-
guard let postCompileJobs = incrementalCompilationState.postCompileJobs
910-
else {
911-
fatalError("planning must have finished by now")
912-
}
913-
try execute(jobs: postCompileJobs)
891+
try executor.execute(
892+
workload: .init(allJobs, incrementalCompilationState),
893+
delegate: createToolExecutionDelegate(),
894+
numParallelJobs: numParallelJobs ?? 1,
895+
forceResponseFiles: forceResponseFiles,
896+
recordedInputModificationDates: recordedInputModificationDates)
914897
}
915898

916899
private func printBindings(_ job: Job) {

Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ struct ToolExecutionDelegate: JobExecutionDelegate {
7373
}
7474

7575
buildRecordInfo?.jobFinished(job: job, result: result)
76-
incrementalCompilationState?.jobFinished(job: job, result: result)
7776

7877
switch mode {
7978
case .regular, .verbose:

Sources/SwiftDriver/Execution/DriverExecutor.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,23 @@ public protocol DriverExecutor {
2222
forceResponseFiles: Bool,
2323
recordedInputModificationDates: [TypedVirtualPath: Date]) throws -> ProcessResult
2424

25+
/// Execute multiple jobs, tracking job status using the provided execution delegate.
26+
/// Pass in the `IncrementalCompilationState` to allow for incremental compilation.
27+
func execute(workload: DriverExecutorWorkload,
28+
delegate: JobExecutionDelegate,
29+
numParallelJobs: Int,
30+
forceResponseFiles: Bool,
31+
recordedInputModificationDates: [TypedVirtualPath: Date]
32+
) throws
33+
2534
/// Execute multiple jobs, tracking job status using the provided execution delegate.
2635
func execute(jobs: [Job],
2736
delegate: JobExecutionDelegate,
2837
numParallelJobs: Int,
2938
forceResponseFiles: Bool,
3039
recordedInputModificationDates: [TypedVirtualPath: Date]
3140
) throws
32-
41+
3342
/// Launch a process with the given command line and report the result.
3443
@discardableResult
3544
func checkNonZeroExit(args: String..., environment: [String: String]) throws -> String
@@ -38,6 +47,15 @@ public protocol DriverExecutor {
3847
func description(of job: Job, forceResponseFiles: Bool) throws -> String
3948
}
4049

50+
public enum DriverExecutorWorkload {
51+
case all([Job])
52+
case incremental(IncrementalCompilationState)
53+
54+
init(_ allJobs: [Job], _ incrementalCompilationState: IncrementalCompilationState?) {
55+
self = incrementalCompilationState.map {.incremental($0)} ?? .all(allJobs)
56+
}
57+
}
58+
4159
enum JobExecutionError: Error {
4260
case jobFailedWithNonzeroExitCode(Int, String)
4361
case failedToReadJobOutput
@@ -70,6 +88,21 @@ extension DriverExecutor {
7088
}
7189
}
7290

91+
public func execute(
92+
jobs: [Job],
93+
delegate: JobExecutionDelegate,
94+
numParallelJobs: Int,
95+
forceResponseFiles: Bool,
96+
recordedInputModificationDates: [TypedVirtualPath: Date]
97+
) throws {
98+
try execute(
99+
workload: .all(jobs),
100+
delegate: delegate,
101+
numParallelJobs: numParallelJobs,
102+
forceResponseFiles: forceResponseFiles,
103+
recordedInputModificationDates: recordedInputModificationDates)
104+
}
105+
73106
static func computeReturnCode(exitStatus: ProcessResult.ExitStatus) -> Int {
74107
var returnCode: Int
75108
switch exitStatus {

Sources/SwiftDriver/Incremental Compilation/IncrementalCompilationState.swift

Lines changed: 86 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,44 @@
1212
import TSCBasic
1313
import Foundation
1414
import SwiftOptions
15-
@_spi(Testing) public class IncrementalCompilationState {
15+
public class IncrementalCompilationState {
1616

1717
/// The oracle for deciding what depends on what. Applies to this whole module.
18-
public let moduleDependencyGraph: ModuleDependencyGraph
18+
private let moduleDependencyGraph: ModuleDependencyGraph
1919

2020
/// If non-null outputs information for `-driver-show-incremental` for input path
2121
public let reportIncrementalDecision: ((String, TypedVirtualPath?) -> Void)?
2222

23+
/// All of the pre-compile or compilation job (groups) known to be required, preserving planning order
24+
public private (set) var mandatoryPreOrCompileJobsInOrder = [Job]()
25+
26+
/// All the pre- or compilation job (groups) known to be required, which have not finished yet.
27+
/// (Changes as jobs complete.)
28+
private var unfinishedMandatoryJobs = Set<Job>()
29+
2330
/// Inputs that must be compiled, and swiftDeps processed.
2431
/// When empty, the compile phase is done.
2532
private var pendingInputs = Set<TypedVirtualPath>()
2633

2734
/// Input files that were skipped.
28-
/// May shrink if one of these moves into pendingInputs.
35+
/// May shrink if one of these moves into pendingInputs. In that case, it will be an input to a
36+
/// "newly-discovered" job.
2937
private(set) var skippedCompilationInputs: Set<TypedVirtualPath>
3038

3139
/// Job groups that were skipped.
3240
/// Need groups rather than jobs because a compile that emits bitcode and its backend job must be
3341
/// treated as a unit.
3442
private var skippedCompileGroups = [TypedVirtualPath: [Job]]()
3543

36-
/// Accumulates jobs to be run through compilation
37-
public var preOrCompileJobs = SynchronizedQueue<[Job]?>()
38-
39-
/// Jobs to run after the last compile
40-
/// Nonnil means planning has informed me
41-
internal private(set) var postCompileJobs: [Job]? = nil
44+
/// Jobs to run *after* the last compile, for instance, link-editing.
45+
public private(set) var postCompileJobs = [Job]()
4246

4347
/// A check for reentrancy.
4448
private var amHandlingJobCompletion = false
4549

4650
// MARK: - Creating IncrementalCompilationState if possible
4751
/// Return nil if not compiling incrementally
48-
public init?(
52+
@_spi(Testing) public init?(
4953
buildRecordInfo: BuildRecordInfo?,
5054
compilerMode: CompilerMode,
5155
diagnosticEngine: DiagnosticsEngine,
@@ -165,7 +169,7 @@ fileprivate extension IncrementalCompilationState {
165169
}
166170

167171
extension Diagnostic.Message {
168-
static var warning_incremental_requires_output_file_map: Diagnostic.Message {
172+
fileprivate static var warning_incremental_requires_output_file_map: Diagnostic.Message {
169173
.warning("ignoring -incremental (currently requires an output file map)")
170174
}
171175
static var warning_incremental_requires_build_record_entry: Diagnostic.Message {
@@ -174,18 +178,20 @@ extension Diagnostic.Message {
174178
"output file map has no master dependencies entry under \(FileType.swiftDeps)"
175179
)
176180
}
177-
static func remark_incremental_compilation_disabled(because why: String) -> Diagnostic.Message {
181+
fileprivate static func remark_incremental_compilation_disabled(because why: String) -> Diagnostic.Message {
178182
.remark("Incremental compilation has been disabled, because \(why)")
179183
}
180-
static func remark_incremental_compilation(because why: String) -> Diagnostic.Message {
184+
fileprivate static func remark_incremental_compilation(because why: String) -> Diagnostic.Message {
181185
.remark("Incremental compilation: \(why)")
182186
}
183187
}
184188

185189

186-
// MARK: - Scheduling the first wave
190+
// MARK: - Scheduling the first wave, i.e. the mandatory pre- and compile jobs
187191

188192
extension IncrementalCompilationState {
193+
194+
/// Figure out which compilation inputs are *not* mandatory
189195
private static func computeSkippedCompilationInputs(
190196
inputFiles: [TypedVirtualPath],
191197
buildRecordInfo: BuildRecordInfo,
@@ -357,43 +363,49 @@ extension IncrementalCompilationState {
357363

358364
// MARK: - Scheduling
359365
extension IncrementalCompilationState {
366+
/// Remember a job (group) that is before a compile or a compile itself.
367+
/// (A group also includes the "backend" jobs for bitcode.)
360368
/// Decide if a job can be skipped, and register accordingly
361-
func addPreOrCompileJobGroups(_ groups: [[Job]]) {
362-
var wereAnyJobsScheduled = false
363-
for group in groups {
369+
func addPreOrCompileJobGroups(_ groups: [[Job]],
370+
formBatchedJobs: ([Job]) throws -> [Job]
371+
) throws {
372+
let mandatoryPreOrCompileJobs = groups.flatMap { group -> [Job] in
364373
if let firstJob = group.first, isSkipped(firstJob) {
365374
recordSkippedGroup(group)
375+
return []
366376
}
367-
else {
368-
schedule(group: group)
369-
wereAnyJobsScheduled = true
370-
}
377+
return group
371378
}
372-
if !wereAnyJobsScheduled {
373-
finishedWithCompilations()
379+
let batchedMandatoryPreOrCompileJobs = try formBatchedJobs(mandatoryPreOrCompileJobs)
380+
scheduleMandatoryPreOrCompile(jobs: batchedMandatoryPreOrCompileJobs)
381+
}
382+
383+
/// Remember that `group` (a compilation and possibly bitcode generation)
384+
/// must definitely be executed.
385+
private func scheduleMandatoryPreOrCompile(jobs: [Job]) {
386+
if let report = reportIncrementalDecision {
387+
for job in jobs {
388+
report("Queuing \(job.descriptionForLifecycle)", nil)
389+
}
374390
}
391+
mandatoryPreOrCompileJobsInOrder.append(contentsOf: jobs)
392+
unfinishedMandatoryJobs.formUnion(jobs)
393+
let mandatoryCompilationInputs = jobs
394+
.flatMap {$0.kind == .compile ? $0.primaryInputs : []}
395+
pendingInputs.formUnion(mandatoryCompilationInputs)
375396
}
376397

377-
func isSkipped(_ job: Job) -> Bool {
398+
/// Decide if this job does not need to run, unless some yet-to-be-discovered dependency changes.
399+
private func isSkipped(_ job: Job) -> Bool {
378400
guard job.kind == .compile else {
379401
return false
380402
}
381-
func isInputSkipped(_ p: TypedVirtualPath) -> Bool {
382-
skippedCompilationInputs.contains(p)
383-
}
384-
guard let jobCanBeSkipped = job.primaryInputs.first.map(isInputSkipped)
385-
else {
386-
return false
387-
}
388-
// Should only be one primary here, but check anyway
389-
assert(
390-
job.primaryInputs.dropFirst().allSatisfy {
391-
isInputSkipped($0) == jobCanBeSkipped}
392-
)
393-
return jobCanBeSkipped
403+
assert(job.primaryInputs.count <= 1, "Jobs should not be batched here.")
404+
return job.primaryInputs.first.map(skippedCompilationInputs.contains) ?? false
394405
}
395406

396-
func recordSkippedGroup(_ group: [Job]) {
407+
/// Remember that this job-group will be skipped (but may be needed later)
408+
private func recordSkippedGroup(_ group: [Job]) {
397409
let job = group.first!
398410
for input in job.primaryInputs {
399411
if let _ = skippedCompileGroups.updateValue(group, forKey: input) {
@@ -402,26 +414,9 @@ extension IncrementalCompilationState {
402414
}
403415
}
404416

405-
func schedule(group: [Job]) {
406-
schedule(preOrCompileJobs: group)
407-
}
408-
/// Put job in queue for execution
409-
func schedule(preOrCompileJobs jobs: [Job]) {
410-
if let report = reportIncrementalDecision {
411-
for job in jobs {
412-
report("Queuing \(job.descriptionForLifecycle)", nil)
413-
}
414-
}
415-
let primaryCompilationInputs = jobs
416-
.flatMap {$0.kind == .compile ? $0.primaryInputs : []}
417-
pendingInputs.formUnion(primaryCompilationInputs)
418-
preOrCompileJobs.enqueue(jobs)
419-
}
420-
421-
/// Remember a job that runs after all compile jobs
417+
/// Remember a job that runs after all compile jobs, e.g., ld
422418
func addPostCompileJobs(_ jobs: [Job]) {
423-
assert(postCompileJobs == nil, "Should only be called once")
424-
postCompileJobs = jobs
419+
self.postCompileJobs = jobs
425420
for job in jobs {
426421
if let report = reportIncrementalDecision {
427422
for input in job.primaryInputs {
@@ -431,31 +426,48 @@ extension IncrementalCompilationState {
431426
}
432427
}
433428

434-
/// Update the incremental build state when a job finishes:
435-
/// Read it's swiftDeps files and queue up any required discovered jobs.
436-
func jobFinished(job finishedJob: Job, result: ProcessResult) {
429+
/// `job` just finished. Update state, and return the skipped compile job (groups) that are now known to be needed.
430+
/// If no more compiles are needed, return nil.
431+
/// Careful: job may not be primary.
432+
public func getJobsDiscoveredToBeNeededAfterFinishing(
433+
job finishedJob: Job, result: ProcessResult)
434+
-> [Job]? {
437435
defer {
438436
amHandlingJobCompletion = false
439437
}
440438
assert(!amHandlingJobCompletion, "was reentered, need to synchronize")
441439
amHandlingJobCompletion = true
442440

441+
unfinishedMandatoryJobs.remove(finishedJob)
442+
if finishedJob.kind == .compile {
443+
finishedJob.primaryInputs.forEach {
444+
if pendingInputs.remove($0) == nil {
445+
fatalError("\($0) input to newly-finished \(finishedJob) should have been pending")
446+
}
447+
}
448+
}
449+
450+
// Find and deal with inputs that how need to be compiled
443451
let discoveredInputs = collectInputsDiscovered(from: finishedJob)
452+
assert(Set(discoveredInputs).isDisjoint(with: finishedJob.primaryInputs),
453+
"Primaries should not overlap secondaries.")
454+
skippedCompilationInputs.subtract(discoveredInputs)
455+
pendingInputs.formUnion(discoveredInputs)
456+
444457
if let report = reportIncrementalDecision {
445458
for input in discoveredInputs {
446459
report("Queuing because of dependencies discovered later:", input)
447460
}
448461
}
449-
schedule(compilationInputs: discoveredInputs)
450-
finishedJob.primaryInputs.forEach {pendingInputs.remove($0)}
451-
if pendingInputs.isEmpty {
452-
finishedWithCompilations()
462+
if pendingInputs.isEmpty && unfinishedMandatoryJobs.isEmpty {
463+
// no more compilations are possible
464+
return nil
453465
}
466+
return getJobsFor(discoveredCompilationInputs: discoveredInputs)
454467
}
455468

456-
private func collectInputsDiscovered(
457-
from job: Job
458-
) -> [TypedVirtualPath] {
469+
/// After `job` finished find out which inputs must compiled that were not known to need compilation before
470+
private func collectInputsDiscovered(from job: Job) -> [TypedVirtualPath] {
459471
Array(
460472
Set(
461473
job.primaryInputs.flatMap {
@@ -467,11 +479,15 @@ extension IncrementalCompilationState {
467479
.sorted {$0.file.name < $1.file.name}
468480
}
469481

470-
private func schedule(compilationInputs inputs: [TypedVirtualPath]) {
471-
let jobs = inputs.flatMap { input -> [Job] in
482+
/// Find the jobs that now must be run that were not originally known to be needed.
483+
private func getJobsFor(
484+
discoveredCompilationInputs inputs: [TypedVirtualPath]
485+
) -> [Job] {
486+
inputs.flatMap { input -> [Job] in
472487
if let group = skippedCompileGroups.removeValue(forKey: input) {
473488
let primaryInputs = group.first!.primaryInputs
474-
skippedCompilationInputs.subtract(primaryInputs)
489+
assert(primaryInputs.count == 1)
490+
assert(primaryInputs[0] == input)
475491
reportIncrementalDecision?("Scheduling discovered", input)
476492
return group
477493
}
@@ -480,11 +496,5 @@ extension IncrementalCompilationState {
480496
return []
481497
}
482498
}
483-
schedule(preOrCompileJobs: jobs)
484-
}
485-
486-
func finishedWithCompilations() {
487-
preOrCompileJobs.enqueue(nil)
488499
}
489500
}
490-

0 commit comments

Comments
 (0)