Skip to content

File Hashing for Incremental Builds #1923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions Sources/SwiftDriver/Driver/Driver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import struct TSCBasic.ByteString
import struct TSCBasic.Diagnostic
import struct TSCBasic.FileInfo
import struct TSCBasic.RelativePath
import struct TSCBasic.SHA256
import var TSCBasic.localFileSystem
import var TSCBasic.stderrStream
import var TSCBasic.stdoutStream
Expand All @@ -44,6 +45,18 @@ extension Driver.ErrorDiagnostics: CustomStringConvertible {
}
}

public struct FileMetadata {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be marked @_spi(Testing)? I don't think it needs to be API

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid this needs to be exposed in the DriverExecutor protocol, so I think it needs to be public.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I forgot this showed up in that API. We may need a strategy to stage in the changes to the DriverExecutor protocol so they don't break https://github.com/swiftlang/swift-build, or make synchronized changes to the repos. Maybe you could keep the old execute requirements around temporarily for default implementations of the new ones to call?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't realise there was an API here exposed to swift-build.

I've added a commit that restores the original methods to DriverExecutor, with an extension that gives default implementations of the new methods, and added in definitions with fatalError() if these legacy methods are called on the implementations defined in swift-driver itself.

3ecc734

public let mTime: TimePoint
public let hash: String?
init(mTime: TimePoint, hash: String? = nil) {
self.mTime = mTime
if let hash = hash, !hash.isEmpty {
self.hash = hash
} else {
self.hash = nil
}
}
}

/// The Swift driver.
public struct Driver {
Expand Down Expand Up @@ -209,8 +222,8 @@ public struct Driver {
/// The set of input files
@_spi(Testing) public let inputFiles: [TypedVirtualPath]

/// The last time each input file was modified, recorded at the start of the build.
@_spi(Testing) public let recordedInputModificationDates: [TypedVirtualPath: TimePoint]
/// The last time each input file was modified, and the file's SHA256 hash, recorded at the start of the build.
@_spi(Testing) public let recordedInputMetadata: [TypedVirtualPath: FileMetadata]

/// The mapping from input files to output files for each kind.
let outputFileMap: OutputFileMap?
Expand Down Expand Up @@ -960,11 +973,20 @@ public struct Driver {
// Classify and collect all of the input files.
let inputFiles = try Self.collectInputFiles(&self.parsedOptions, diagnosticsEngine: diagnosticsEngine, fileSystem: self.fileSystem)
self.inputFiles = inputFiles
self.recordedInputModificationDates = .init(uniqueKeysWithValues:
Set(inputFiles).compactMap {
guard let modTime = try? fileSystem
.lastModificationTime(for: $0.file) else { return nil }
return ($0, modTime)

let incrementalFileHashes = parsedOptions.hasFlag(positive: .enableIncrementalFileHashing,
negative: .disableIncrementalFileHashing,
default: false)
self.recordedInputMetadata = .init(uniqueKeysWithValues:
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
guard let modTime = try? fileSystem.lastModificationTime(for: inputFile.file) else { return nil }
if incrementalFileHashes {
guard let data = try? fileSystem.readFileContents(inputFile.file) else { return nil }
let hash = SHA256().hash(data).hexadecimalRepresentation
return (inputFile, FileMetadata(mTime: modTime, hash: hash))
} else {
return (inputFile, FileMetadata(mTime: modTime))
}
})

do {
Expand Down Expand Up @@ -1083,7 +1105,7 @@ public struct Driver {
outputFileMap: outputFileMap,
incremental: self.shouldAttemptIncrementalCompilation,
parsedOptions: parsedOptions,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)

self.supportedFrontendFlags =
try Self.computeSupportedCompilerArgs(of: self.toolchain,
Expand Down Expand Up @@ -1933,7 +1955,7 @@ extension Driver {
}
try executor.execute(job: inPlaceJob,
forceResponseFiles: forceResponseFiles,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)
}

// If requested, warn for options that weren't used by the driver after the build is finished.
Expand Down Expand Up @@ -1978,7 +2000,7 @@ extension Driver {
delegate: jobExecutionDelegate,
numParallelJobs: numParallelJobs ?? 1,
forceResponseFiles: forceResponseFiles,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)
}

public func writeIncrementalBuildInformation(_ jobs: [Job]) {
Expand Down
51 changes: 49 additions & 2 deletions Sources/SwiftDriver/Execution/DriverExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,25 @@ public protocol DriverExecutor {

/// Execute a single job and capture the output.
@discardableResult
func execute(job: Job,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]) throws -> ProcessResult

func execute(job: Job,
forceResponseFiles: Bool,
recordedInputModificationDates: [TypedVirtualPath: TimePoint]) throws -> ProcessResult


/// Execute multiple jobs, tracking job status using the provided execution delegate.
/// Pass in the `IncrementalCompilationState` to allow for incremental compilation.
/// Pass in the `InterModuleDependencyGraph` to allow for module dependency tracking.
func execute(workload: DriverExecutorWorkload,
delegate: JobExecutionDelegate,
numParallelJobs: Int,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
) throws

func execute(workload: DriverExecutorWorkload,
delegate: JobExecutionDelegate,
numParallelJobs: Int,
Expand All @@ -38,6 +50,13 @@ public protocol DriverExecutor {
) throws

/// Execute multiple jobs, tracking job status using the provided execution delegate.
func execute(jobs: [Job],
delegate: JobExecutionDelegate,
numParallelJobs: Int,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
) throws

func execute(jobs: [Job],
delegate: JobExecutionDelegate,
numParallelJobs: Int,
Expand All @@ -53,6 +72,34 @@ public protocol DriverExecutor {
func description(of job: Job, forceResponseFiles: Bool) throws -> String
}

extension DriverExecutor {
public func execute(job: Job,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]) throws -> ProcessResult
{
return try execute(job: job, forceResponseFiles: forceResponseFiles, recordedInputModificationDates: recordedInputMetadata.mapValues { $0.mTime })
}


public func execute(workload: DriverExecutorWorkload,
delegate: JobExecutionDelegate,
numParallelJobs: Int,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
) throws {
try execute(workload: workload, delegate: delegate, numParallelJobs: numParallelJobs, forceResponseFiles: forceResponseFiles, recordedInputModificationDates: recordedInputMetadata.mapValues { $0.mTime })
}

public func execute(jobs: [Job],
delegate: JobExecutionDelegate,
numParallelJobs: Int,
forceResponseFiles: Bool,
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
) throws {
try execute(jobs: jobs, delegate: delegate, numParallelJobs: numParallelJobs, forceResponseFiles: forceResponseFiles, recordedInputModificationDates: recordedInputMetadata.mapValues { $0.mTime })
}
}

public struct DriverExecutorWorkload {
public let continueBuildingAfterErrors: Bool
public enum Kind {
Expand Down Expand Up @@ -96,10 +143,10 @@ extension DriverExecutor {
func execute<T: Decodable>(job: Job,
capturingJSONOutputAs outputType: T.Type,
forceResponseFiles: Bool,
recordedInputModificationDates: [TypedVirtualPath: TimePoint]) throws -> T {
recordedInputMetadata: [TypedVirtualPath: FileMetadata]) throws -> T {
let result = try execute(job: job,
forceResponseFiles: forceResponseFiles,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)

if (result.exitStatus != .terminated(code: EXIT_SUCCESS)) {
let returnCode = Self.computeReturnCode(exitStatus: result.exitStatus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public extension Driver {
try self.executor.execute(job: preScanJob,
capturingJSONOutputAs: InterModuleDependencyImports.self,
forceResponseFiles: forceResponseFiles,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)
}
return imports
}
Expand Down Expand Up @@ -343,7 +343,7 @@ public extension Driver {
try self.executor.execute(job: scannerJob,
capturingJSONOutputAs: InterModuleDependencyGraph.self,
forceResponseFiles: forceResponseFiles,
recordedInputModificationDates: recordedInputModificationDates)
recordedInputMetadata: recordedInputMetadata)
}
return dependencyGraph
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftDriver/IncrementalCompilation/BuildRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension BuildRecord {
init(jobs: [Job],
finishedJobResults: [BuildRecordInfo.JobResult],
skippedInputs: Set<TypedVirtualPath>?,
compilationInputModificationDates: [TypedVirtualPath: TimePoint],
compilationInputModificationDates: [TypedVirtualPath: FileMetadata],
actualSwiftVersion: String,
argsHash: String,
timeBeforeFirstJob: TimePoint,
Expand All @@ -57,10 +57,10 @@ extension BuildRecord {
entry.job.inputsGeneratingCode.map { ($0, entry.result) }
})
let inputInfosArray = compilationInputModificationDates
.map { input, modDate -> (VirtualPath, InputInfo) in
.map { input, metadata -> (VirtualPath, InputInfo) in
let status = InputInfo.Status( wasSkipped: skippedInputs?.contains(input),
jobResult: jobResultsByInput[input])
return (input.file, InputInfo(status: status, previousModTime: modDate))
return (input.file, InputInfo(status: status, previousModTime: metadata.mTime, hash: metadata.hash))
}

self.init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import class Dispatch.DispatchQueue
@_spi(Testing) public let actualSwiftVersion: String
@_spi(Testing) public let timeBeforeFirstJob: TimePoint
let diagnosticEngine: DiagnosticsEngine
let compilationInputModificationDates: [TypedVirtualPath: TimePoint]
let compilationInputModificationDates: [TypedVirtualPath: FileMetadata]
private var explicitModuleDependencyGraph: InterModuleDependencyGraph? = nil

private var finishedJobResults = [JobResult]()
Expand All @@ -64,7 +64,7 @@ import class Dispatch.DispatchQueue
actualSwiftVersion: String,
timeBeforeFirstJob: TimePoint,
diagnosticEngine: DiagnosticsEngine,
compilationInputModificationDates: [TypedVirtualPath: TimePoint])
compilationInputModificationDates: [TypedVirtualPath: FileMetadata])
{
self.buildRecordPath = buildRecordPath
self.fileSystem = fileSystem
Expand All @@ -85,7 +85,7 @@ import class Dispatch.DispatchQueue
outputFileMap: OutputFileMap?,
incremental: Bool,
parsedOptions: ParsedOptions,
recordedInputModificationDates: [TypedVirtualPath: TimePoint]
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
) {
// Cannot write a buildRecord without a path.
guard let buildRecordPath = try? Self.computeBuildRecordPath(
Expand All @@ -99,7 +99,7 @@ import class Dispatch.DispatchQueue
}
let currentArgsHash = BuildRecordArguments.computeHash(parsedOptions)
let compilationInputModificationDates =
recordedInputModificationDates.filter { input, _ in
recordedInputMetadata.filter { input, _ in
input.type.isPartOfSwiftCompilation
}

Expand Down
23 changes: 16 additions & 7 deletions Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extension IncrementalCompilationState {
let showJobLifecycle: Bool
let alwaysRebuildDependents: Bool
let interModuleDependencyGraph: InterModuleDependencyGraph?
let useHashes: Bool
/// If non-null outputs information for `-driver-show-incremental` for input path
private let reporter: Reporter?

Expand All @@ -45,6 +46,7 @@ extension IncrementalCompilationState {
self.showJobLifecycle = driver.showJobLifecycle
self.alwaysRebuildDependents = initialState.incrementalOptions.contains(
.alwaysRebuildDependents)
self.useHashes = initialState.incrementalOptions.contains(.useFileHashesInModuleDependencyGraph)
self.interModuleDependencyGraph = interModuleDependencyGraph
self.reporter = reporter
}
Expand Down Expand Up @@ -301,8 +303,9 @@ extension IncrementalCompilationState.FirstWaveComputer {
/// The status of the input file.
let status: InputInfo.Status
/// If `true`, the modification time of this input matches the modification
/// time recorded from the prior build in the build record.
let datesMatch: Bool
/// time recorded from the prior build in the build record, or the hash of
/// its contents match.
let metadataMatch: Bool
}

// Find the inputs that have changed since last compilation, or were marked as needed a build
Expand All @@ -311,16 +314,21 @@ extension IncrementalCompilationState.FirstWaveComputer {
) -> [ChangedInput] {
jobsInPhases.compileJobs.compactMap { job in
let input = job.primaryInputs[0]
let modDate = buildRecordInfo.compilationInputModificationDates[input] ?? .distantFuture
let metadata = buildRecordInfo.compilationInputModificationDates[input] ?? FileMetadata(mTime: .distantFuture)
let inputInfo = outOfDateBuildRecord.inputInfos[input.file]
let previousCompilationStatus = inputInfo?.status ?? .newlyAdded
let previousModTime = inputInfo?.previousModTime
let previousHash = inputInfo?.hash

assert(metadata.hash != nil || !useHashes)

switch previousCompilationStatus {
case .upToDate where modDate == previousModTime:
case .upToDate where metadata.mTime == previousModTime:
reporter?.report("May skip current input:", input)
return nil

case .upToDate where useHashes && (metadata.hash == previousHash):
reporter?.report("May skip current input (identical hash):", input)
return nil
case .upToDate:
reporter?.report("Scheduling changed input", input)
case .newlyAdded:
Expand All @@ -330,9 +338,10 @@ extension IncrementalCompilationState.FirstWaveComputer {
case .needsNonCascadingBuild:
reporter?.report("Scheduling noncascading build", input)
}
let metadataMatch = metadata.mTime == previousModTime || (useHashes && metadata.hash == previousHash)
return ChangedInput(typedFile: input,
status: previousCompilationStatus,
datesMatch: modDate == previousModTime)
metadataMatch: metadataMatch )
}
}

Expand Down Expand Up @@ -381,7 +390,7 @@ extension IncrementalCompilationState.FirstWaveComputer {
) -> [TypedVirtualPath] {
changedInputs.compactMap { changedInput in
let inputIsUpToDate =
changedInput.datesMatch && !inputsMissingOutputs.contains(changedInput.typedFile)
changedInput.metadataMatch && !inputsMissingOutputs.contains(changedInput.typedFile)
let basename = changedInput.typedFile.file.basename

// If we're asked to always rebuild dependents, all we need to do is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ extension IncrementalCompilationState {
/// Enables additional handling of explicit module build artifacts:
/// Additional reading and writing of the inter-module dependency graph.
public static let explicitModuleBuild = Options(rawValue: 1 << 6)

/// Enables use of file hashes as a fallback in the case that a timestamp
/// change might invalidate a node
public static let useFileHashesInModuleDependencyGraph = Options(rawValue: 1 << 7)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ extension IncrementalCompilationState {
if driver.parsedOptions.contains(.driverExplicitModuleBuild) {
options.formUnion(.explicitModuleBuild)
}
if driver.parsedOptions.hasFlag(positive: .enableIncrementalFileHashing,
negative: .disableIncrementalFileHashing,
default: false) {
options.formUnion(.useFileHashesInModuleDependencyGraph)
}

return options
}
}
Expand Down Expand Up @@ -128,6 +134,9 @@ extension IncrementalCompilationState {
@_spi(Testing) public var emitDependencyDotFileAfterEveryImport: Bool {
options.contains(.emitDependencyDotFileAfterEveryImport)
}
@_spi(Testing) public var useFileHashesInModuleDependencyGraph: Bool {
options.contains(.useFileHashesInModuleDependencyGraph)
}

@_spi(Testing) public init(
_ options: Options,
Expand Down
6 changes: 5 additions & 1 deletion Sources/SwiftDriver/IncrementalCompilation/InputInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

import struct TSCBasic.ProcessResult
import struct TSCBasic.SHA256

/// Contains information about the current status of an input to the incremental
/// build.
Expand All @@ -25,9 +26,12 @@ import struct TSCBasic.ProcessResult
/// The last known modification time of this input.
/*@_spi(Testing)*/ public let previousModTime: TimePoint

/*@_spi(Testing)*/ public init(status: Status, previousModTime: TimePoint) {
/*@_spi(Testing)*/ public let hash: String?

/*@_spi(Testing)*/ public init(status: Status, previousModTime: TimePoint, hash: String?) {
self.status = status
self.previousModTime = previousModTime
self.hash = hash
}
}

Expand Down
Loading