12
12
import TSCBasic
13
13
import Foundation
14
14
import SwiftOptions
15
- @ _spi ( Testing ) public class IncrementalCompilationState {
15
+ public class IncrementalCompilationState {
16
16
17
17
/// The oracle for deciding what depends on what. Applies to this whole module.
18
- public let moduleDependencyGraph : ModuleDependencyGraph
18
+ private let moduleDependencyGraph : ModuleDependencyGraph
19
19
20
20
/// If non-null outputs information for `-driver-show-incremental` for input path
21
21
public let reportIncrementalDecision : ( ( String , TypedVirtualPath ? ) -> Void ) ?
22
22
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
+
23
30
/// Inputs that must be compiled, and swiftDeps processed.
24
31
/// When empty, the compile phase is done.
25
32
private var pendingInputs = Set < TypedVirtualPath > ( )
26
33
27
34
/// 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.
29
37
private( set) var skippedCompilationInputs : Set < TypedVirtualPath >
30
38
31
39
/// Job groups that were skipped.
32
40
/// Need groups rather than jobs because a compile that emits bitcode and its backend job must be
33
41
/// treated as a unit.
34
42
private var skippedCompileGroups = [ TypedVirtualPath: [ Job] ] ( )
35
43
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] ( )
42
46
43
47
/// A check for reentrancy.
44
48
private var amHandlingJobCompletion = false
45
49
46
50
// MARK: - Creating IncrementalCompilationState if possible
47
51
/// Return nil if not compiling incrementally
48
- public init ? (
52
+ @ _spi ( Testing ) public init ? (
49
53
buildRecordInfo: BuildRecordInfo ? ,
50
54
compilerMode: CompilerMode ,
51
55
diagnosticEngine: DiagnosticsEngine ,
@@ -165,7 +169,7 @@ fileprivate extension IncrementalCompilationState {
165
169
}
166
170
167
171
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 {
169
173
. warning( " ignoring -incremental (currently requires an output file map) " )
170
174
}
171
175
static var warning_incremental_requires_build_record_entry : Diagnostic . Message {
@@ -174,18 +178,20 @@ extension Diagnostic.Message {
174
178
" output file map has no master dependencies entry under \( FileType . swiftDeps) "
175
179
)
176
180
}
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 {
178
182
. remark( " Incremental compilation has been disabled, because \( why) " )
179
183
}
180
- static func remark_incremental_compilation( because why: String ) -> Diagnostic . Message {
184
+ fileprivate static func remark_incremental_compilation( because why: String ) -> Diagnostic . Message {
181
185
. remark( " Incremental compilation: \( why) " )
182
186
}
183
187
}
184
188
185
189
186
- // MARK: - Scheduling the first wave
190
+ // MARK: - Scheduling the first wave, i.e. the mandatory pre- and compile jobs
187
191
188
192
extension IncrementalCompilationState {
193
+
194
+ /// Figure out which compilation inputs are *not* mandatory
189
195
private static func computeSkippedCompilationInputs(
190
196
inputFiles: [ TypedVirtualPath ] ,
191
197
buildRecordInfo: BuildRecordInfo ,
@@ -357,43 +363,49 @@ extension IncrementalCompilationState {
357
363
358
364
// MARK: - Scheduling
359
365
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.)
360
368
/// 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
364
373
if let firstJob = group. first, isSkipped ( firstJob) {
365
374
recordSkippedGroup ( group)
375
+ return [ ]
366
376
}
367
- else {
368
- schedule ( group: group)
369
- wereAnyJobsScheduled = true
370
- }
377
+ return group
371
378
}
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
+ }
374
390
}
391
+ mandatoryPreOrCompileJobsInOrder. append ( contentsOf: jobs)
392
+ unfinishedMandatoryJobs. formUnion ( jobs)
393
+ let mandatoryCompilationInputs = jobs
394
+ . flatMap { $0. kind == . compile ? $0. primaryInputs : [ ] }
395
+ pendingInputs. formUnion ( mandatoryCompilationInputs)
375
396
}
376
397
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 {
378
400
guard job. kind == . compile else {
379
401
return false
380
402
}
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
394
405
}
395
406
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 ] ) {
397
409
let job = group. first!
398
410
for input in job. primaryInputs {
399
411
if let _ = skippedCompileGroups. updateValue ( group, forKey: input) {
@@ -402,26 +414,9 @@ extension IncrementalCompilationState {
402
414
}
403
415
}
404
416
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
422
418
func addPostCompileJobs( _ jobs: [ Job ] ) {
423
- assert ( postCompileJobs == nil , " Should only be called once " )
424
- postCompileJobs = jobs
419
+ self . postCompileJobs = jobs
425
420
for job in jobs {
426
421
if let report = reportIncrementalDecision {
427
422
for input in job. primaryInputs {
@@ -431,31 +426,48 @@ extension IncrementalCompilationState {
431
426
}
432
427
}
433
428
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 ] ? {
437
435
defer {
438
436
amHandlingJobCompletion = false
439
437
}
440
438
assert ( !amHandlingJobCompletion, " was reentered, need to synchronize " )
441
439
amHandlingJobCompletion = true
442
440
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
443
451
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
+
444
457
if let report = reportIncrementalDecision {
445
458
for input in discoveredInputs {
446
459
report ( " Queuing because of dependencies discovered later: " , input)
447
460
}
448
461
}
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
453
465
}
466
+ return getJobsFor ( discoveredCompilationInputs: discoveredInputs)
454
467
}
455
468
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 ] {
459
471
Array (
460
472
Set (
461
473
job. primaryInputs. flatMap {
@@ -467,11 +479,15 @@ extension IncrementalCompilationState {
467
479
. sorted { $0. file. name < $1. file. name}
468
480
}
469
481
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
472
487
if let group = skippedCompileGroups. removeValue ( forKey: input) {
473
488
let primaryInputs = group. first!. primaryInputs
474
- skippedCompilationInputs. subtract ( primaryInputs)
489
+ assert ( primaryInputs. count == 1 )
490
+ assert ( primaryInputs [ 0 ] == input)
475
491
reportIncrementalDecision ? ( " Scheduling discovered " , input)
476
492
return group
477
493
}
@@ -480,11 +496,5 @@ extension IncrementalCompilationState {
480
496
return [ ]
481
497
}
482
498
}
483
- schedule ( preOrCompileJobs: jobs)
484
- }
485
-
486
- func finishedWithCompilations( ) {
487
- preOrCompileJobs. enqueue ( nil )
488
499
}
489
500
}
490
-
0 commit comments