1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
// Created by David Ungar on 7/28/21.
//
import XCTest
@_spi(Testing) import SwiftDriver
import TSCBasic
class IncrementalBuildPerformanceTests: XCTestCase {
enum WhatToMeasure { case readingSwiftDeps, writing, readingPriors }
/// Test the cost of reading `swiftdeps` files without doing a full build. Use the files in "TestInputs/SampleSwiftDeps"
///
/// When doing an incremental but clean build, after every file is compiled, its `swiftdeps` file must be
/// deserialized and integrated into the `ModuleDependencyGraph`.
/// This test allows us to profile an optimize this work. (Set up the scheme to run optimized code.)
/// It reads and integrages every swiftdeps file in a given directory.
///
/// This test relies on sample `swiftdeps` files to be present in `<project-folder>/TestInputs/SampleSwiftDeps`.
/// If the serialization format changes, they will need to be regenerated.
/// To regenerate them:
/// `cd` to the package directory, then:
/// `rm TestInputs/SampleSwiftDeps/*; rm -rf .build; swift build; find .build -name \*.swiftdeps -a -exec cp \{\} TestInputs/SampleSwiftDeps \;`
func testCleanBuildSwiftDepsPerformance() throws {
try testPerformance(.readingSwiftDeps)
}
func testSavingPriorsPerformance() throws {
try testPerformance(.writing)
}
func testReadingPriorsPerformance() throws {
try testPerformance(.readingPriors)
}
func testPerformance(_ whatToMeasure: WhatToMeasure) throws {
#if !os(macOS)
// rdar://81411914
throw XCTSkip()
#else
let packageRootPath = try AbsolutePath(validating: #file)
.parentDirectory
.parentDirectory
.parentDirectory
let swiftDepsDirectoryPath = packageRootPath.appending(components: "TestInputs", "SampleSwiftDeps")
#if DEBUG
let limit = 5 // Just a few times to be sure it works
#else
let limit = 100 // This is the real test, optimized code.
#endif
try test(swiftDepsDirectory: swiftDepsDirectoryPath.pathString, atMost: limit, whatToMeasure)
#endif
}
/// Test the cost of reading `swiftdeps` files without doing a full build.
///
/// When doing an incremental but clean build, after every file is compiled, its `swiftdeps` file must be
/// deserialized and integrated into the `ModuleDependencyGraph`.
/// This test allows us to profile an optimize this work. (Set up the scheme to run optimized code.)
/// It reads and integrages every swiftdeps file in a given directory.
/// - Parameters:
/// - swiftDepsDirectory: where the swiftdeps files are, either absolute, or relative to the current directory
/// - limit: the maximum number of swiftdeps files to process.
func test(swiftDepsDirectory: String, atMost limit: Int = .max, _ whatToMeasure: WhatToMeasure) throws {
let (outputFileMap, inputs) = try createOFMAndInputs(swiftDepsDirectory, atMost: limit)
let info = IncrementalCompilationState.IncrementalDependencyAndInputSetup
.mock(options: [], outputFileMap: outputFileMap)
let g = ModuleDependencyGraph.createForSimulatingCleanBuild(info.buildRecordInfo.buildRecord([], []), info)
g.blockingConcurrentAccessOrMutation {
switch whatToMeasure {
case .readingSwiftDeps:
measure {readSwiftDeps(for: inputs, into: g)}
case .writing:
readSwiftDeps(for: inputs, into: g)
measure {
_ = ModuleDependencyGraph.Serializer.serialize(
g,
g.buildRecord,
ModuleDependencyGraph.serializedGraphVersion)
}
case .readingPriors:
readSwiftDeps(for: inputs, into: g)
let data = ModuleDependencyGraph.Serializer.serialize(
g,
g.buildRecord,
ModuleDependencyGraph.serializedGraphVersion)
measure {
try? XCTAssertNoThrow(ModuleDependencyGraph.deserialize(data, info: info))
}
}
}
}
/// Build the `OutputFileMap` and input vector for ``testCleanBuildSwiftDepsPerformance(_, atMost)``
private func createOFMAndInputs(_ swiftDepsDirectory: String,
atMost limit: Int
) throws -> (OutputFileMap, [SwiftSourceFile]) {
let workingDirectory = localFileSystem.currentWorkingDirectory!
let swiftDepsDirPath = try VirtualPath.init(path: swiftDepsDirectory).resolvedRelativePath(base: workingDirectory).absolutePath!
let withoutExtensions: ArraySlice<Substring> = try localFileSystem.getDirectoryContents(swiftDepsDirPath)
.compactMap {
fileName -> Substring? in
guard let suffixRange = fileName.range(of: ".swiftdeps"),
suffixRange.upperBound == fileName.endIndex
else {
return nil
}
let withoutExtension = fileName.prefix(upTo: suffixRange.lowerBound)
guard !withoutExtension.hasSuffix("-master") else { return nil }
return withoutExtension
}
.sorted()
.prefix(limit)
print("reading", withoutExtensions.count, "swiftdeps files")
func mkPath( _ name: Substring, _ type: FileType) -> TypedVirtualPath {
TypedVirtualPath(
file: VirtualPath.absolute(swiftDepsDirPath.appending(component: name + "." + type.rawValue)).intern(),
type: type)
}
let inputs = withoutExtensions.map {mkPath($0, .swift)}.swiftSourceFiles
let swiftDepsVPs = withoutExtensions.map {mkPath($0, .swiftDeps)}
let entries = Dictionary(
uniqueKeysWithValues:
zip(inputs, swiftDepsVPs).map {input, swiftDeps in
(input.fileHandle, [swiftDeps.type: swiftDeps.fileHandle])
})
return (OutputFileMap(entries: entries), inputs)
}
/// Read the `swiftdeps` files for each input into a `ModuleDependencyGraph`
private func readSwiftDeps(for inputs: [SwiftSourceFile], into g: ModuleDependencyGraph) {
let result = inputs.reduce(into: Set()) { invalidatedInputs, primaryInput in
// too verbose: print("processing", primaryInput)
invalidatedInputs.formUnion(g.collectInputsRequiringCompilation(byCompiling: primaryInput)!)
}
.subtracting(inputs) // have already compiled these
XCTAssertEqual(result.count, 0, "Should be no invalid inputs left")
}
}
|