Skip to content

Commit 82a2ef6

Browse files
committed
[Incremental Builds] Separately check whether we can skip 'emit-module' on an incremental module-only build
Resolves rdar://151626629
1 parent cc4bda0 commit 82a2ef6

File tree

3 files changed

+106
-9
lines changed

3 files changed

+106
-9
lines changed

Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,23 @@ extension IncrementalCompilationState.FirstWaveComputer {
134134
jobCreatingPch: jobCreatingPch)
135135

136136
// In the case where there are no compilation jobs to run on this build (no source-files were changed),
137-
// we can skip running `beforeCompiles` jobs if we also ensure that none of the `afterCompiles` jobs
138-
// have any dependencies on them.
139-
let skipAllJobs = batchedCompilationJobs.isEmpty ? !nonVerifyAfterCompileJobsDependOnBeforeCompileJobs() : false
140-
let beforeCompileJobs = skipAllJobs ? [] : jobsInPhases.beforeCompiles
141-
var skippedNonCompileJobs = skipAllJobs ? jobsInPhases.beforeCompiles : []
137+
// and the emit-module task does not need to be re-run, we can skip running `beforeCompiles` jobs if we
138+
// also ensure that none of the `afterCompiles` jobs have any dependencies on them.
139+
let skippingAllCompileJobs = batchedCompilationJobs.isEmpty
140+
let skipEmitModuleJobs = try skippingAllCompileJobs && computeCanSkipEmitModuleTasks(buildRecord)
141+
let skipAllJobs = skippingAllCompileJobs && skipEmitModuleJobs && !nonVerifyAfterCompileJobsDependOnBeforeCompileJobs()
142+
143+
let beforeCompileJobs: [Job]
144+
var skippedNonCompileJobs: [Job] = []
145+
if skipAllJobs {
146+
beforeCompileJobs = []
147+
skippedNonCompileJobs = jobsInPhases.beforeCompiles
148+
} else if skipEmitModuleJobs {
149+
beforeCompileJobs = jobsInPhases.beforeCompiles.filter { $0.kind != .emitModule }
150+
skippedNonCompileJobs.append(contentsOf: jobsInPhases.beforeCompiles.filter { $0.kind == .emitModule })
151+
} else {
152+
beforeCompileJobs = jobsInPhases.beforeCompiles
153+
}
142154

143155
// Schedule emitModule job together with verify module interface job.
144156
let afterCompileJobs = jobsInPhases.afterCompiles.compactMap { job -> Job? in
@@ -170,6 +182,27 @@ extension IncrementalCompilationState.FirstWaveComputer {
170182
}
171183
}
172184

185+
/// Figure out if the emit-module tasks are *not* mandatory. This functionality only runs if there are not actual
186+
/// compilation tasks to be run in this build, for example on an emit-module-only build.
187+
private func computeCanSkipEmitModuleTasks(_ buildRecord: BuildRecord) throws -> Bool {
188+
guard let emitModuleJob = jobsInPhases.beforeCompiles.first(where: { $0.kind == .emitModule }) else {
189+
return false // Nothing to skip, so no special handling is required
190+
}
191+
// If a non-emit-module task exists in 'beforeCompiles', it may be another kind of
192+
// changed dependency so we should re-run the module task as well
193+
guard jobsInPhases.beforeCompiles.allSatisfy({ $0.kind == .emitModule }) else {
194+
return false
195+
}
196+
// If any of the outputs do not exist, they must be re-computed
197+
guard try emitModuleJob.outputs.allSatisfy({ try fileSystem.exists($0.file) }) else {
198+
return false
199+
}
200+
201+
// Ensure that no output is older than any of the inputs
202+
let oldestOutputModTime: TimePoint = try emitModuleJob.outputs.map { try fileSystem.lastModificationTime(for: $0.file) }.min() ?? .distantPast
203+
return try emitModuleJob.inputs.swiftSourceFiles.allSatisfy({ try fileSystem.lastModificationTime(for: $0.typedFile.file) < oldestOutputModTime })
204+
}
205+
173206
/// Figure out which compilation inputs are *not* mandatory at the start
174207
private func computeInitiallySkippedCompilationInputs(
175208
inputsInvalidatedByExternals: TransitivelyInvalidatedSwiftSourceFileSet,
@@ -178,7 +211,7 @@ extension IncrementalCompilationState.FirstWaveComputer {
178211
) -> Set<TypedVirtualPath> {
179212
let allCompileJobs = jobsInPhases.compileJobs
180213
// Input == source file
181-
let changedInputs = computeChangedInputs(moduleDependencyGraph, buildRecord)
214+
let changedInputs = computeChangedInputs(buildRecord)
182215

183216
if let reporter = reporter {
184217
for input in inputsInvalidatedByExternals {
@@ -274,7 +307,6 @@ extension IncrementalCompilationState.FirstWaveComputer {
274307

275308
// Find the inputs that have changed since last compilation, or were marked as needed a build
276309
private func computeChangedInputs(
277-
_ moduleDependencyGraph: ModuleDependencyGraph,
278310
_ outOfDateBuildRecord: BuildRecord
279311
) -> [ChangedInput] {
280312
jobsInPhases.compileJobs.compactMap { job in

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,6 @@ final class ExplicitModuleBuildTests: XCTestCase {
913913
try incrementalDriver.run(jobs: incrementalJobs)
914914
XCTAssertFalse(incrementalDriver.diagnosticEngine.hasErrors)
915915
let state = try XCTUnwrap(incrementalDriver.incrementalCompilationState)
916-
XCTAssertTrue(state.mandatoryJobsInOrder.contains { $0.kind == .emitModule })
917916
XCTAssertTrue(state.jobsAfterCompiles.contains { $0.kind == .verifyModuleInterface })
918917

919918
// TODO: emitModule job should run again if interface is deleted.
@@ -923,7 +922,6 @@ final class ExplicitModuleBuildTests: XCTestCase {
923922
var reDriver = try Driver(args: invocationArguments + ["-color-diagnostics"])
924923
let _ = try reDriver.planBuild()
925924
let reState = try XCTUnwrap(reDriver.incrementalCompilationState)
926-
XCTAssertFalse(reState.mandatoryJobsInOrder.contains { $0.kind == .emitModule })
927925
XCTAssertFalse(reState.jobsAfterCompiles.contains { $0.kind == .verifyModuleInterface })
928926
}
929927
}

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,55 @@ extension IncrementalCompilationTests {
645645
linking
646646
}
647647
}
648+
649+
func testExplicitIncrementalEmitModuleOnly() throws {
650+
guard let sdkArgumentsForTesting = try Driver.sdkArgumentsForTesting()
651+
else {
652+
throw XCTSkip("Cannot perform this test on this host")
653+
}
654+
655+
let args = [
656+
"swiftc",
657+
"-module-name", module,
658+
"-emit-module", "-emit-module-path",
659+
derivedDataPath.appending(component: module + ".swiftmodule").pathString,
660+
"-incremental",
661+
"-driver-show-incremental",
662+
"-driver-show-job-lifecycle",
663+
"-save-temps",
664+
"-output-file-map", OFM.pathString,
665+
"-no-color-diagnostics"
666+
] + inputPathsAndContents.map {$0.0.pathString}.sorted() + explicitBuildArgs + sdkArgumentsForTesting
667+
668+
// Initial build
669+
_ = try doABuildWithoutExpectations(arguments: args)
670+
671+
// Subsequent build, ensure module does not get re-emitted since inputs have not changed
672+
_ = try doABuild(
673+
whenAutolinking: autolinkLifecycleExpectedDiags,
674+
arguments: args
675+
) {
676+
readGraph
677+
explicitIncrementalScanReuseCache(serializedDepScanCachePath.pathString)
678+
explicitIncrementalScanCacheSerialized(serializedDepScanCachePath.pathString)
679+
queuingInitial("main", "other")
680+
}
681+
682+
touch("main")
683+
touch("other")
684+
// Subsequent build, ensure module re-emitted since inputs changed
685+
_ = try doABuild(
686+
whenAutolinking: autolinkLifecycleExpectedDiags,
687+
arguments: args
688+
) {
689+
readGraph
690+
explicitIncrementalScanReuseCache(serializedDepScanCachePath.pathString)
691+
explicitIncrementalScanCacheSerialized(serializedDepScanCachePath.pathString)
692+
queuingInitial("main", "other")
693+
emittingModule(module)
694+
schedulingPostCompileJobs
695+
}
696+
}
648697
}
649698

650699
extension IncrementalCompilationTests {
@@ -1770,6 +1819,14 @@ extension IncrementalCompilationTests {
17701819
}
17711820
}
17721821

1822+
private func doABuild(
1823+
whenAutolinking autolinkExpectedDiags: [Diagnostic.Message],
1824+
arguments: [String],
1825+
@DiagsBuilder expecting expectedDiags: () -> [Diagnostic.Message]
1826+
) throws -> Driver {
1827+
try doABuild(whenAutolinking: autolinkExpectedDiags, expecting: expectedDiags(), arguments: arguments)
1828+
}
1829+
17731830
private func doABuildWithoutExpectations(arguments: [String]) throws -> Driver {
17741831
// If not checking, print out the diagnostics
17751832
let diagnosticEngine = DiagnosticsEngine(handlers: [
@@ -1859,6 +1916,16 @@ extension DiagVerifiable {
18591916
@DiagsBuilder func explicitDependencyModuleOlderThanInput(_ dependencyModuleName: String) -> [Diagnostic.Message] {
18601917
"Dependency module \(dependencyModuleName) is older than input file"
18611918
}
1919+
@DiagsBuilder func startEmitModule(_ moduleName: String) -> [Diagnostic.Message] {
1920+
"Starting Emitting module for \(moduleName)"
1921+
}
1922+
@DiagsBuilder func finishEmitModule(_ moduleName: String) -> [Diagnostic.Message] {
1923+
"Finished Emitting module for \(moduleName)"
1924+
}
1925+
@DiagsBuilder func emittingModule(_ moduleName: String) -> [Diagnostic.Message] {
1926+
startEmitModule(moduleName)
1927+
finishEmitModule(moduleName)
1928+
}
18621929
@DiagsBuilder func startCompilingExplicitClangDependency(_ dependencyModuleName: String) -> [Diagnostic.Message] {
18631930
"Starting Compiling Clang module \(dependencyModuleName)"
18641931
}

0 commit comments

Comments
 (0)