Skip to content

Commit a6d6d13

Browse files
committed
[cas] Enable the CAS size limiting functionality for Swift caching
rdar://148443907
1 parent 0810d2c commit a6d6d13

File tree

5 files changed

+201
-6
lines changed

5 files changed

+201
-6
lines changed

Sources/SWBCore/LibSwiftDriver/LibSwiftDriver.swift

+17
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,15 @@ public final class LibSwiftDriver {
461461
case path(Path)
462462
case library(libSwiftScanPath: Path)
463463

464+
public var compilerOrLibraryPath: Path {
465+
switch self {
466+
case .path(let path):
467+
return path
468+
case .library(let path):
469+
return path
470+
}
471+
}
472+
464473
public var description: String {
465474
switch self {
466475
case .path(let path):
@@ -753,6 +762,14 @@ public final class SwiftCASDatabases {
753762
self.cas = cas
754763
}
755764

765+
public var supportsSizeManagement: Bool { cas.supportsSizeManagement }
766+
767+
public func getStorageSize() throws -> Int64? { try cas.getStorageSize() }
768+
769+
public func setSizeLimit(_ size: Int64) throws { try cas.setSizeLimit(size) }
770+
771+
public func prune() throws { try cas.prune() }
772+
756773
public func queryCacheKey(_ key: String, globally: Bool) async throws -> SwiftCachedCompilation? {
757774
guard let comp = try await cas.queryCacheKey(key, globally: globally) else { return nil }
758775
return SwiftCachedCompilation(comp, key: key)

Sources/SWBTaskExecution/DynamicTaskSpecs/CompilationCachingDataPruner.swift

+71-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ package final class CompilationCachingDataPruner: Sendable {
119119
diagnostic: Diagnostic(
120120
behavior: .remark,
121121
location: .unknown,
122-
data: DiagnosticData("cache size (\(dbSize)) larger than size limit (\(sizeLimit)")
122+
data: DiagnosticData("cache size (\(dbSize)) larger than size limit (\(sizeLimit))")
123123
),
124124
for: activityID,
125125
signature: signature
@@ -142,6 +142,75 @@ package final class CompilationCachingDataPruner: Sendable {
142142
}
143143
}
144144

145+
package func pruneCAS(
146+
_ casDBs: SwiftCASDatabases,
147+
key: ClangCachingPruneDataTaskKey,
148+
activityReporter: any ActivityReporter,
149+
fileSystem fs: any FSProxy
150+
) {
151+
let casOpts = key.casOptions
152+
guard casOpts.limitingStrategy != .discarded else {
153+
return // No need to prune, CAS directory is getting deleted.
154+
}
155+
let inserted = state.withLock { $0.prunedCASes.insert(key).inserted }
156+
guard inserted else {
157+
return // already pruned
158+
}
159+
160+
startedAction()
161+
let serializer = MsgPackSerializer()
162+
key.serialize(to: serializer)
163+
let signatureCtx = InsecureHashContext()
164+
signatureCtx.add(string: "SwiftCachingPruneData")
165+
signatureCtx.add(bytes: serializer.byteString)
166+
let signature = signatureCtx.signature
167+
168+
let casPath = casOpts.casPath.str
169+
let swiftscanPath = key.path.str
170+
171+
// Avoiding the swift concurrency variant because it may lead to starvation when `waitForCompletion()`
172+
// blocks on such tasks. Before using a swift concurrency task here make sure there's no deadlock
173+
// when setting `LIBDISPATCH_COOPERATIVE_POOL_STRICT`.
174+
queue.async {
175+
activityReporter.withActivity(
176+
ruleInfo: "SwiftCachingPruneData \(casPath) \(swiftscanPath)",
177+
executionDescription: "Swift caching pruning \(casPath) using \(swiftscanPath)",
178+
signature: signature,
179+
target: nil,
180+
parentActivity: nil)
181+
{ activityID in
182+
let status: BuildOperationTaskEnded.Status
183+
do {
184+
let dbSize = try casDBs.getStorageSize()
185+
let sizeLimit = try computeCASSizeLimit(casOptions: casOpts, dbSize: dbSize.map{Int($0)}, fileSystem: fs)
186+
if casOpts.enableDiagnosticRemarks, let dbSize, let sizeLimit, sizeLimit < dbSize {
187+
activityReporter.emit(
188+
diagnostic: Diagnostic(
189+
behavior: .remark,
190+
location: .unknown,
191+
data: DiagnosticData("cache size (\(dbSize)) larger than size limit (\(sizeLimit))")
192+
),
193+
for: activityID,
194+
signature: signature
195+
)
196+
}
197+
try casDBs.setSizeLimit(Int64(sizeLimit ?? 0))
198+
try casDBs.prune()
199+
status = .succeeded
200+
} catch {
201+
activityReporter.emit(
202+
diagnostic: Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(error.localizedDescription)),
203+
for: activityID,
204+
signature: signature
205+
)
206+
status = .failed
207+
}
208+
return status
209+
}
210+
self.finishedAction()
211+
}
212+
}
213+
145214
package func pruneCAS(
146215
_ toolchainCAS: ToolchainCAS,
147216
key: ClangCachingPruneDataTaskKey,
@@ -188,7 +257,7 @@ package final class CompilationCachingDataPruner: Sendable {
188257
diagnostic: Diagnostic(
189258
behavior: .remark,
190259
location: .unknown,
191-
data: DiagnosticData("cache size (\(dbSize)) larger than size limit (\(sizeLimit)")
260+
data: DiagnosticData("cache size (\(dbSize)) larger than size limit (\(sizeLimit))")
192261
),
193262
for: activityID,
194263
signature: signature

Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift

+11
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,17 @@ public final class SwiftDriverJobTaskAction: TaskAction, BuildValueValidatingTas
494494
if let casOpts = payload.casOptions, casOpts.enableIntegratedCacheQueries {
495495
let swiftModuleDependencyGraph = dynamicExecutionDelegate.operationContext.swiftModuleDependencyGraph
496496
cas = try swiftModuleDependencyGraph.getCASDatabases(casOptions: casOpts, compilerLocation: payload.compilerLocation)
497+
498+
let casKey = ClangCachingPruneDataTaskKey(
499+
path: payload.compilerLocation.compilerOrLibraryPath,
500+
casOptions: casOpts
501+
)
502+
dynamicExecutionDelegate.operationContext.compilationCachingDataPruner.pruneCAS(
503+
cas!,
504+
key: casKey,
505+
activityReporter: dynamicExecutionDelegate,
506+
fileSystem: executionDelegate.fs
507+
)
497508
} else {
498509
cas = nil
499510
}

Sources/SWBTestSupport/BuildOperationTester.swift

+19
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,25 @@ package final class BuildOperationTester {
631631
return nil
632632
}
633633

634+
package func getDiagnosticMessageForTask(_ pattern: StringPattern, kind: DiagnosticKind, task: Task) -> String? {
635+
for (index, event) in self.events.enumerated() {
636+
switch event {
637+
case .taskHadEvent(let eventTask, event: .hadDiagnostic(let diagnostic)) where diagnostic.behavior == kind:
638+
guard eventTask == task else {
639+
continue
640+
}
641+
let message = diagnostic.formatLocalizedDescription(.debugWithoutBehavior, task: eventTask)
642+
if pattern ~= message {
643+
_eventList.remove(at: index)
644+
return message
645+
}
646+
default:
647+
continue
648+
}
649+
}
650+
return nil
651+
}
652+
634653
package func check(_ pattern: StringPattern, kind: BuildOperationTester.DiagnosticKind, failIfNotFound: Bool, sourceLocation: SourceLocation, checkDiagnostic: (Diagnostic) -> Bool) -> Bool {
635654
let found = (getDiagnosticMessage(pattern, kind: kind, checkDiagnostic: checkDiagnostic) != nil)
636655
if !found, failIfNotFound {

Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift

+83-4
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,93 @@ fileprivate struct SwiftCompilationCachingTests: CoreBasedTests {
126126
#expect(try readMetrics("two").contains("\"swiftCacheHits\":\(numCompile),\"swiftCacheMisses\":0"))
127127
}
128128
}
129+
130+
@Test(.requireSDKs(.macOS))
131+
func swiftCASLimiting() async throws {
132+
try await withTemporaryDirectory { (tmpDirPath: Path) async throws -> Void in
133+
let testWorkspace = try await TestWorkspace(
134+
"Test",
135+
sourceRoot: tmpDirPath.join("Test"),
136+
projects: [
137+
TestProject(
138+
"aProject",
139+
groupTree: TestGroup(
140+
"Sources",
141+
children: [
142+
TestFile("main.swift"),
143+
]),
144+
buildConfigurations: [
145+
TestBuildConfiguration(
146+
"Debug",
147+
buildSettings: [
148+
"PRODUCT_NAME": "$(TARGET_NAME)",
149+
"SDKROOT": "macosx",
150+
"SWIFT_VERSION": swiftVersion,
151+
"SWIFT_ENABLE_EXPLICIT_MODULES": "YES",
152+
"SWIFT_ENABLE_COMPILE_CACHE": "YES",
153+
"COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS": "YES",
154+
"COMPILATION_CACHE_LIMIT_SIZE": "1",
155+
"COMPILATION_CACHE_CAS_PATH": tmpDirPath.join("CompilationCache").str,
156+
"DSTROOT": tmpDirPath.join("dstroot").str,
157+
]),
158+
],
159+
targets: [
160+
TestStandardTarget(
161+
"tool",
162+
type: .framework,
163+
buildPhases: [
164+
TestSourcesBuildPhase([
165+
"main.swift",
166+
]),
167+
]
168+
)
169+
])
170+
])
171+
let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
172+
173+
try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/main.swift")) {
174+
$0 <<< "let x = 1\n"
175+
}
176+
177+
try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in
178+
results.checkTask(.matchRuleType("SwiftCompile")) { results.checkKeyQueryCacheMiss($0) }
179+
}
180+
try await tester.checkBuild(runDestination: .macOS, buildCommand: .cleanBuildFolder(style: .regular), body: { _ in })
181+
182+
// Update the source file and rebuild.
183+
try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/main.swift")) {
184+
$0 <<< "let x = 2\n"
185+
}
186+
try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in
187+
results.checkTask(.matchRuleType("SwiftCompile")) { results.checkKeyQueryCacheMiss($0) }
188+
}
189+
try await tester.checkBuild(runDestination: .macOS, buildCommand: .cleanBuildFolder(style: .regular), body: { _ in })
190+
191+
// Revert the source change and rebuild. It should still be a cache miss because of CAS size limiting.
192+
try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/main.swift")) {
193+
$0 <<< "let x = 1\n"
194+
}
195+
try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in
196+
results.checkTask(.matchRuleType("SwiftCompile")) { results.checkKeyQueryCacheMiss($0) }
197+
}
198+
}
199+
}
129200
}
130201

131202
extension BuildOperationTester.BuildResults {
132-
fileprivate func checkKeyQueryCacheMiss(_ task: Task, file: StaticString = #file, line: UInt = #line) {
133-
checkRemark(.contains("cache key query miss"))
203+
fileprivate func checkKeyQueryCacheMiss(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) {
204+
let found = (getDiagnosticMessageForTask(.contains("cache miss"), kind: .remark, task: task) != nil)
205+
guard found else {
206+
Issue.record("Unable to find cache miss diagnostic for task \(task)", sourceLocation: sourceLocation)
207+
return
208+
}
134209
}
135210

136-
fileprivate func checkKeyQueryCacheHit(_ task: Task, file: StaticString = #file, line: UInt = #line) {
137-
checkRemark(.contains("cache key query hit"))
211+
fileprivate func checkKeyQueryCacheHit(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) {
212+
let found = (getDiagnosticMessageForTask(.contains("cache found for key"), kind: .remark, task: task) != nil)
213+
guard found else {
214+
Issue.record("Unable to find cache hit diagnostic for task \(task)", sourceLocation: sourceLocation)
215+
return
216+
}
138217
}
139218
}

0 commit comments

Comments
 (0)