Skip to content

Commit 9a4465c

Browse files
committed
[Explicit Module Builds][Incremental Builds] Only re-built module dependencies
which have changed or whose dependencies have changed. Resolves rdar://113638007
1 parent 41f491d commit 9a4465c

14 files changed

+329
-82
lines changed

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift

+8-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ import func TSCBasic.topologicalSort
5454
}
5555

5656
extension InterModuleDependencyGraph {
57+
var topologicalSorting: [ModuleDependencyId] {
58+
get throws {
59+
try topologicalSort(Array(modules.keys),
60+
successors: { try moduleInfo(of: $0).directDependencies! })
61+
}
62+
}
63+
5764
/// Compute a set of modules that are "reachable" (form direct or transitive dependency)
5865
/// from each module in the graph.
5966
/// This routine relies on the fact that the dependency graph is acyclic. A lack of cycles means
@@ -65,9 +72,7 @@ extension InterModuleDependencyGraph {
6572
/// }
6673
/// }
6774
func computeTransitiveClosure() throws -> [ModuleDependencyId : Set<ModuleDependencyId>] {
68-
let topologicalIdList =
69-
try topologicalSort(Array(modules.keys),
70-
successors: { try moduleInfo(of: $0).directDependencies! })
75+
let topologicalIdList = try self.topologicalSorting
7176
// This structure will contain the final result
7277
var transitiveClosureMap =
7378
topologicalIdList.reduce(into: [ModuleDependencyId : Set<ModuleDependencyId>]()) {

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ public enum ModuleDependencyId: Hashable {
3030
case .clang(let name): return name
3131
}
3232
}
33+
34+
internal var moduleNameForDiagnostic: String {
35+
switch self {
36+
case .swift(let name): return name
37+
case .swiftPlaceholder(let name): return name + "(placeholder)"
38+
case .swiftPrebuiltExternal(let name): return name + "(swiftmodule)"
39+
case .clang(let name): return name + "(pcm)"
40+
}
41+
}
3342
}
3443

3544
extension ModuleDependencyId: Codable {

Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift

+122-8
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ extension IncrementalCompilationState {
2525
let fileSystem: FileSystem
2626
let showJobLifecycle: Bool
2727
let alwaysRebuildDependents: Bool
28-
let rebuildExplicitModuleDependencies: Bool
28+
let interModuleDependencyGraph: InterModuleDependencyGraph?
29+
let explicitModuleDependenciesGuaranteedUpToDate: Bool
2930
/// If non-null outputs information for `-driver-show-incremental` for input path
3031
private let reporter: Reporter?
3132

3233
@_spi(Testing) public init(
3334
initialState: IncrementalCompilationState.InitialStateForPlanning,
3435
jobsInPhases: JobsInPhases,
3536
driver: Driver,
37+
interModuleDependencyGraph: InterModuleDependencyGraph?,
3638
reporter: Reporter?
3739
) {
3840
self.moduleDependencyGraph = initialState.graph
@@ -44,8 +46,9 @@ extension IncrementalCompilationState {
4446
self.showJobLifecycle = driver.showJobLifecycle
4547
self.alwaysRebuildDependents = initialState.incrementalOptions.contains(
4648
.alwaysRebuildDependents)
47-
self.rebuildExplicitModuleDependencies =
48-
initialState.maybeUpToDatePriorInterModuleDependencyGraph != nil ? false : true
49+
self.interModuleDependencyGraph = interModuleDependencyGraph
50+
self.explicitModuleDependenciesGuaranteedUpToDate =
51+
initialState.upToDatePriorInterModuleDependencyGraph != nil ? true : false
4952
self.reporter = reporter
5053
}
5154

@@ -118,11 +121,7 @@ extension IncrementalCompilationState.FirstWaveComputer {
118121
: compileGroups[input]
119122
}
120123

121-
// If module dependencies are known to be up-to-date, do not rebuild them
122-
let mandatoryBeforeCompilesJobs = self.rebuildExplicitModuleDependencies ?
123-
jobsInPhases.beforeCompiles :
124-
jobsInPhases.beforeCompiles.filter { $0.kind != .generatePCM && $0.kind != .compileModuleFromInterface }
125-
124+
let mandatoryBeforeCompilesJobs = try computeMandatoryBeforeCompilesJobs()
126125
let batchedCompilationJobs = try batchJobFormer.formBatchedJobs(
127126
mandatoryCompileGroupsInOrder.flatMap {$0.allJobs()},
128127
showJobLifecycle: showJobLifecycle)
@@ -136,6 +135,121 @@ extension IncrementalCompilationState.FirstWaveComputer {
136135
mandatoryJobsInOrder: mandatoryJobsInOrder)
137136
}
138137

138+
/// We must determine if any of the module dependencies require re-compilation
139+
/// Since we know that a prior dependency graph was not completely up-to-date,
140+
/// there must be at least *some* dependencies that require being re-built.
141+
///
142+
/// If a dependency is deemed as requiring a re-build, then every module
143+
/// between it and the root (source module being built by this driver
144+
/// instance) must also be re-built.
145+
private func computeInvalidatedModuleDependencies(on moduleDependencyGraph: InterModuleDependencyGraph)
146+
throws -> Set<ModuleDependencyId> {
147+
let mainModuleInfo = moduleDependencyGraph.mainModule
148+
var modulesRequiringRebuild: Set<ModuleDependencyId> = []
149+
var validatedModules: Set<ModuleDependencyId> = []
150+
// Scan from the main module's dependencies to avoid reporting
151+
// the main module itself in the results.
152+
for dependencyId in mainModuleInfo.directDependencies ?? [] {
153+
try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId,
154+
pathSoFar: [], visitedValidated: &validatedModules,
155+
modulesRequiringRebuild: &modulesRequiringRebuild)
156+
}
157+
158+
reporter?.reportExplicitDependencyReBuildSet(Array(modulesRequiringRebuild))
159+
return modulesRequiringRebuild
160+
}
161+
162+
/// Perform a postorder DFS to locate modules which are out-of-date with respect
163+
/// to their inputs. Upon encountering such a module, add it to the set of invalidated
164+
/// modules, along with the path from the root to this module.
165+
private func outOfDateModuleScan(on moduleDependencyGraph: InterModuleDependencyGraph,
166+
from moduleId: ModuleDependencyId,
167+
pathSoFar: [ModuleDependencyId],
168+
visitedValidated: inout Set<ModuleDependencyId>,
169+
modulesRequiringRebuild: inout Set<ModuleDependencyId>) throws {
170+
let moduleInfo = try moduleDependencyGraph.moduleInfo(of: moduleId)
171+
let isMainModule = moduleId == .swift(moduleDependencyGraph.mainModuleName)
172+
173+
// Routine to invalidate the path from root to this node
174+
let invalidatePath = { (modulesRequiringRebuild: inout Set<ModuleDependencyId>) in
175+
if let reporter {
176+
for pathModuleId in pathSoFar {
177+
if !modulesRequiringRebuild.contains(pathModuleId) &&
178+
!isMainModule {
179+
let pathModuleInfo = try moduleDependencyGraph.moduleInfo(of: pathModuleId)
180+
reporter.reportExplicitDependencyWillBeReBuilt(pathModuleId.moduleNameForDiagnostic,
181+
reason: "Invalidated by downstream dependency")
182+
}
183+
}
184+
}
185+
modulesRequiringRebuild.formUnion(pathSoFar)
186+
}
187+
188+
// Routine to invalidate this node and the path that led to it
189+
let invalidateOutOfDate = { (modulesRequiringRebuild: inout Set<ModuleDependencyId>) in
190+
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Out-of-date")
191+
modulesRequiringRebuild.insert(moduleId)
192+
try invalidatePath(&modulesRequiringRebuild)
193+
}
194+
195+
// Visit the module's dependencies
196+
for dependencyId in moduleInfo.directDependencies ?? [] {
197+
if !visitedValidated.contains(dependencyId) {
198+
let newPath = pathSoFar + [moduleId]
199+
try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId, pathSoFar: newPath,
200+
visitedValidated: &visitedValidated,
201+
modulesRequiringRebuild: &modulesRequiringRebuild)
202+
}
203+
}
204+
205+
if modulesRequiringRebuild.contains(moduleId) {
206+
try invalidatePath(&modulesRequiringRebuild)
207+
} else if try !IncrementalCompilationState.IncrementalDependencyAndInputSetup.verifyModuleDependencyUpToDate(moduleID: moduleId, moduleInfo: moduleInfo,
208+
fileSystem: fileSystem, reporter: reporter) {
209+
try invalidateOutOfDate(&modulesRequiringRebuild)
210+
} else {
211+
// Only if this module is known to be up-to-date with respect to its inputs
212+
// and dependencies do we mark it as visited. We may need to re-visit
213+
// out-of-date modules in order to collect all possible paths to them.
214+
visitedValidated.insert(moduleId)
215+
}
216+
}
217+
218+
/// In an explicit module build, filter out dependency module pre-compilation tasks
219+
/// for modules up-to-date from a prior compile.
220+
private func computeMandatoryBeforeCompilesJobs() throws -> [Job] {
221+
// In an implicit module build, we have nothing to filter/compute here
222+
guard let moduleDependencyGraph = interModuleDependencyGraph else {
223+
return jobsInPhases.beforeCompiles
224+
}
225+
226+
// If a prior compile's dependency graph was fully up-to-date, we can skip
227+
// re-building all dependency modules.
228+
guard !self.explicitModuleDependenciesGuaranteedUpToDate else {
229+
return jobsInPhases.beforeCompiles.filter { $0.kind != .generatePCM &&
230+
$0.kind != .compileModuleFromInterface }
231+
}
232+
233+
// Determine which module pre-build jobs must be re-run
234+
let modulesRequiringReBuild =
235+
try computeInvalidatedModuleDependencies(on: moduleDependencyGraph)
236+
237+
// Filter the `.generatePCM` and `.compileModuleFromInterface` jobs for
238+
// modules which do *not* need re-building.
239+
let mandatoryBeforeCompilesJobs = jobsInPhases.beforeCompiles.filter { job in
240+
switch job.kind {
241+
case .generatePCM:
242+
return modulesRequiringReBuild.contains(.clang(job.moduleName))
243+
case .compileModuleFromInterface:
244+
return modulesRequiringReBuild.contains(.swift(job.moduleName))
245+
default:
246+
return true
247+
}
248+
}
249+
250+
return mandatoryBeforeCompilesJobs
251+
}
252+
139253
/// Determine if any of the jobs in the `afterCompiles` group depend on outputs produced by jobs in
140254
/// `beforeCompiles` group, which are not also verification jobs.
141255
private func nonVerifyAfterCompileJobsDependOnBeforeCompileJobs() -> Bool {

Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState+Extensions.swift

+13-5
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ extension IncrementalCompilationState {
5050
let graph: ModuleDependencyGraph
5151
/// Information about the last known compilation, incl. the location of build artifacts such as the dependency graph.
5252
let buildRecordInfo: BuildRecordInfo
53-
/// Record about the compiled module's module dependencies from the last compile.
54-
let maybeUpToDatePriorInterModuleDependencyGraph: InterModuleDependencyGraph?
53+
/// Record about the compiled module's explicit module dependencies from a prior compile.
54+
let upToDatePriorInterModuleDependencyGraph: InterModuleDependencyGraph?
5555
/// A set of inputs invalidated by external changes.
5656
let inputsInvalidatedByExternals: TransitivelyInvalidatedSwiftSourceFileSet
5757
/// Compiler options related to incremental builds.
@@ -299,9 +299,17 @@ extension IncrementalCompilationState {
299299
}
300300

301301
func reportExplicitDependencyOutOfDate(_ moduleName: String,
302-
outputPath: String,
303-
updatedInputPath: String) {
304-
report("Dependency module \(moduleName) is older than input file \(updatedInputPath) at \(outputPath)")
302+
inputPath: String) {
303+
report("Dependency module \(moduleName) is older than input file \(inputPath)")
304+
}
305+
306+
func reportExplicitDependencyWillBeReBuilt(_ moduleOutputPath: String,
307+
reason: String) {
308+
report("Dependency module '\(moduleOutputPath)' will be re-built: \(reason)")
309+
}
310+
311+
func reportExplicitDependencyReBuildSet(_ modules: [ModuleDependencyId]) {
312+
report("Following explicit module dependencies will be re-built: [\(modules.map { $0.moduleNameForDiagnostic }.sorted().joined(separator: ", "))]")
305313
}
306314

307315
// Emits a remark indicating incremental compilation has been disabled.

Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public final class IncrementalCompilationState {
6868
initialState: initialState,
6969
jobsInPhases: jobsInPhases,
7070
driver: driver,
71+
interModuleDependencyGraph: interModuleDependencyGraph,
7172
reporter: reporter)
7273
.compute(batchJobFormer: &driver)
7374

0 commit comments

Comments
 (0)