Skip to content
Merged
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
27 changes: 6 additions & 21 deletions Sources/SWBBuildService/BuildOperationMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -705,24 +705,12 @@ private final class TaskOutputHandler: TaskOutputDelegate {
}
}

func incrementClangCacheHit() {
self.counters[.clangCacheHits, default: 0] += 1
func incrementCounter(_ counter: BuildOperationMetrics.Counter, by amount: Int) {
self.counters[counter, default: 0] += amount
}

func incrementClangCacheMiss() {
self.counters[.clangCacheMisses, default: 0] += 1
}

func incrementSwiftCacheHit() {
self.counters[.swiftCacheHits, default: 0] += 1
}

func incrementSwiftCacheMiss() {
self.counters[.swiftCacheMisses, default: 0] += 1
}

func incrementTaskCounter(_ counter: BuildOperationMetrics.TaskCounter) {
self.taskCounters[counter, default: 0] += 1
func incrementTaskCounter(_ counter: BuildOperationMetrics.TaskCounter, by amount: Int) {
self.taskCounters[counter, default: 0] += amount
}

var diagnosticsEngine: DiagnosticProducingDelegateProtocolPrivate<DiagnosticsEngine> {
Expand Down Expand Up @@ -944,11 +932,8 @@ private final class DiscardingTaskOutputHandler: TaskOutputDelegate {
func subtaskUpToDate(_ subtask: any ExecutableTask) {}
func previouslyBatchedSubtaskUpToDate(signature: ByteString, target: ConfiguredTarget) {}
func updateResult(_ result: TaskResult) {}
func incrementClangCacheHit() {}
func incrementClangCacheMiss() {}
func incrementSwiftCacheHit() {}
func incrementSwiftCacheMiss() {}
func incrementTaskCounter(_ counter: BuildOperationMetrics.TaskCounter) {}
func incrementCounter(_ counter: BuildOperationMetrics.Counter, by amount: Int) {}
func incrementTaskCounter(_ counter: BuildOperationMetrics.TaskCounter, by amount: Int) {}
}

/// The build output delegate, which sends data back immediately.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBBuildSystem/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ package final class BuildOperation: BuildSystemOperation {
}

// `buildComplete()` should not run within `queue`, otherwise there can be a deadlock during cancelling.
return delegate.buildComplete(self, status: effectiveStatus, delegate: buildOutputDelegate, metrics: .init(counters: aggregatedCounters))
return delegate.buildComplete(self, status: effectiveStatus, delegate: buildOutputDelegate, metrics: .init(counters: aggregatedCounters, taskCounters: aggregatedTaskCounters))
}

func prepareForBuilding() async -> ([String], [String])? {
Expand Down
148 changes: 93 additions & 55 deletions Sources/SWBCore/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
public struct ModuleDependenciesContext: Sendable, SerializableCodable {
public var validate: BooleanWarningLevel
public var validateUnused: BooleanWarningLevel
var moduleDependencies: [ModuleDependency]
public var declared: [ModuleDependency]
var fixItContext: FixItContext?

init(validate: BooleanWarningLevel, validateUnused: BooleanWarningLevel, moduleDependencies: [ModuleDependency], fixItContext: FixItContext? = nil) {
init(validate: BooleanWarningLevel, validateUnused: BooleanWarningLevel, declared: [ModuleDependency], fixItContext: FixItContext? = nil) {
self.validate = validate
self.validateUnused = validateUnused
self.moduleDependencies = moduleDependencies
self.declared = declared
self.fixItContext = fixItContext
}

Expand All @@ -90,7 +90,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
guard validate != .no || validateUnused != .no else { return nil }
let downgrade = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_DEPENDENCIES_DOWNGRADE_ERRORS)
let fixItContext = validate != .no ? ModuleDependenciesContext.FixItContext(settings: settings) : nil
self.init(validate: downgrade ? .yes : validate, validateUnused: validateUnused, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
self.init(validate: downgrade ? .yes : validate, validateUnused: validateUnused, declared: settings.moduleDependencies, fixItContext: fixItContext)
}

public func makeUnsupportedToolchainDiagnostic() -> Diagnostic {
Expand All @@ -111,14 +111,14 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
if fromSwift && $0.importLocations.isEmpty { return false }

// TODO: if the difference is just the access modifier, we emit a new entry, but ultimately our fixit should update the existing entry or emit an error about a conflict
if moduleDependencies.contains($0.0) { return false }
if declared.contains($0.0) { return false }
return true
}
}

public func computeUnusedDependencies(usedModuleNames: Set<String>) -> [ModuleDependency] {
guard validateUnused != .no else { return [] }
return moduleDependencies.filter { !$0.optional && !usedModuleNames.contains($0.name) }
return declared.filter { !$0.optional && !usedModuleNames.contains($0.name) }
}

/// Make diagnostics for missing module dependencies.
Expand Down Expand Up @@ -231,6 +231,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
public struct HeaderDependency: Hashable, Sendable, SerializableCodable {
public let name: String
public let accessLevel: AccessLevel
public let optional: Bool

public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable {
case Private = "private"
Expand All @@ -245,29 +246,25 @@ public struct HeaderDependency: Hashable, Sendable, SerializableCodable {
}
}

public init(name: String, accessLevel: AccessLevel) {
public init(name: String, accessLevel: AccessLevel, optional: Bool) {
self.name = name
self.accessLevel = accessLevel
self.optional = optional
}

public init(entry: String) throws {
var it = entry.split(separator: " ").makeIterator()
switch (it.next(), it.next(), it.next()) {
case (let .some(name), nil, nil):
self.name = String(name)
self.accessLevel = .Private

case (let .some(accessLevel), let .some(name), nil):
self.name = String(name)
self.accessLevel = try AccessLevel(String(accessLevel))

default:
throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
let re = #/((?<accessLevel>private|package|public)\s+)?(?<name>.+)(?<optional>\?)?/#
guard let match = entry.wholeMatch(of: re) else {
throw StubError.error("Invalid header dependency format: \(entry), expected [private|package|public] <name>[?]")
}

self.name = String(match.output.name)
self.accessLevel = try match.output.accessLevel.map { try AccessLevel(String($0)) } ?? .Private
self.optional = match.output.optional != nil
}

public var asBuildSettingEntry: String {
"\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)"
"\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)\(optional ? "?" : "")"
}

public var asBuildSettingEntryQuotedIfNeeded: String {
Expand All @@ -278,63 +275,104 @@ public struct HeaderDependency: Hashable, Sendable, SerializableCodable {

public struct HeaderDependenciesContext: Sendable, SerializableCodable {
public var validate: BooleanWarningLevel
var headerDependencies: [HeaderDependency]
public var validateUnused: BooleanWarningLevel
public var declared: [HeaderDependency]
var fixItContext: FixItContext?

init(validate: BooleanWarningLevel, headerDependencies: [HeaderDependency], fixItContext: FixItContext? = nil) {
init(validate: BooleanWarningLevel, validateUnused: BooleanWarningLevel, declared: [HeaderDependency], fixItContext: FixItContext? = nil) {
self.validate = validate
self.headerDependencies = headerDependencies
self.validateUnused = validateUnused
self.declared = declared
self.fixItContext = fixItContext
}

public init?(settings: Settings) {
let validate = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES)
guard validate != .no else { return nil }
let validateUnused = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_UNUSED_HEADER_DEPENDENCIES)
guard validate != .no || validateUnused != .no else { return nil }
let downgrade = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_DEPENDENCIES_DOWNGRADE_ERRORS)
let fixItContext = HeaderDependenciesContext.FixItContext(settings: settings)
self.init(validate: downgrade ? .yes : validate, headerDependencies: settings.headerDependencies, fixItContext: fixItContext)
self.init(validate: downgrade ? .yes : validate, validateUnused: validateUnused, declared: settings.headerDependencies, fixItContext: fixItContext)
}

public func makeUnsupportedToolchainDiagnostic() -> Diagnostic {
Diagnostic(
behavior: .warning,
location: .unknown,
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES.name)"))
}

/// Compute missing module dependencies.
public func computeMissingAndUnusedDependencies(includes: [Path]) -> (missing: [HeaderDependency], unused: [HeaderDependency]) {
let declaredNames = Set(declared.map { $0.name })

let missing: [HeaderDependency]
if validate != .no {
missing = includes.filter { file in
return !declaredNames.contains(where: { file.ends(with: $0) })
}.map {
// TODO: What if the basename doesn't uniquely identify the header?
HeaderDependency(name: $0.basename, accessLevel: .Private, optional: false)
}
}
else {
missing = []
}

let unused: [HeaderDependency]
if validateUnused != .no {
unused = declared.filter { !$0.optional && !declaredNames.contains($0.name) }
}
else {
unused = []
}

return (missing, unused)
}

/// Make diagnostics for missing header dependencies.
///
/// The compiler tracing information does not provide the include locations or whether they are public imports
/// (which depends on whether the import is in an installed header file).
/// If `includes` is nil, the current toolchain does support the feature to trace imports.
public func makeDiagnostics(includes: [Path]?) -> [Diagnostic] {
guard validate != .no else { return [] }
guard let includes else {
return [Diagnostic(
behavior: .warning,
location: .unknown,
data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES.name)"))]
}

let headerDependencyNames = headerDependencies.map { $0.name }
let missingDeps = includes.filter { file in
return !headerDependencyNames.contains(where: { file.ends(with: $0) })
}.map {
// TODO: What if the basename doesn't uniquely identify the header?
HeaderDependency(name: $0.basename, accessLevel: .Private)
}
public func makeDiagnostics(missingDependencies: [HeaderDependency], unusedDependencies: [HeaderDependency]) -> [Diagnostic] {
let missingDiagnostics: [Diagnostic]
if !missingDependencies.isEmpty {
let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning

guard !missingDeps.isEmpty else { return [] }
let fixIt = fixItContext?.makeFixIt(newHeaders: missingDependencies)
let fixIts = fixIt.map { [$0] } ?? []

let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
let message = "Missing entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(missingDependencies.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"

let fixIt = fixItContext?.makeFixIt(newHeaders: missingDeps)
let fixIts = fixIt.map { [$0] } ?? []
let location: Diagnostic.Location = fixIt.map {
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.HEADER_DEPENDENCIES)

let message = "Missing entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
missingDiagnostics = [Diagnostic(
behavior: behavior,
location: location,
data: DiagnosticData(message),
fixIts: fixIts)]
}
else {
missingDiagnostics = []
}

let location: Diagnostic.Location = fixIt.map {
Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
} ?? Diagnostic.Location.buildSetting(BuiltinMacros.HEADER_DEPENDENCIES)
let unusedDiagnostics: [Diagnostic]
if !unusedDependencies.isEmpty {
let message = "Unused entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(unusedDependencies.map { $0.name }.sorted().joined(separator: " "))"
// TODO location & fixit
unusedDiagnostics = [Diagnostic(
behavior: validateUnused == .yesError ? .error : .warning,
location: .unknown,
data: DiagnosticData(message),
fixIts: [])]
}
else {
unusedDiagnostics = []
}

return [Diagnostic(
behavior: behavior,
location: location,
data: DiagnosticData(message),
fixIts: fixIts)]
return missingDiagnostics + unusedDiagnostics
}

struct FixItContext: Sendable, SerializableCodable {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ public final class BuiltinMacros {
public static let VALIDATE_DEPENDENCIES_DOWNGRADE_ERRORS = BuiltinMacros.declareBooleanMacro("VALIDATE_DEPENDENCIES_DOWNGRADE_ERRORS")
public static let VALIDATE_DEVELOPMENT_ASSET_PATHS = BuiltinMacros.declareEnumMacro("VALIDATE_DEVELOPMENT_ASSET_PATHS") as EnumMacroDeclaration<BooleanWarningLevel>
public static let VALIDATE_HEADER_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_HEADER_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
public static let VALIDATE_UNUSED_HEADER_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_UNUSED_HEADER_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
public static let VALIDATE_MODULE_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_MODULE_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
public static let VALIDATE_UNUSED_MODULE_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_UNUSED_MODULE_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
public static let VECTOR_SUFFIX = BuiltinMacros.declareStringMacro("VECTOR_SUFFIX")
Expand Down Expand Up @@ -2388,6 +2389,7 @@ public final class BuiltinMacros {
VALIDATE_DEVELOPMENT_ASSET_PATHS,
VALIDATE_HEADER_DEPENDENCIES,
VALIDATE_MODULE_DEPENDENCIES,
VALIDATE_UNUSED_HEADER_DEPENDENCIES,
VALIDATE_UNUSED_MODULE_DEPENDENCIES,
VALID_ARCHS,
VECTOR_SUFFIX,
Expand Down
56 changes: 55 additions & 1 deletion Sources/SWBProtocol/BuildOperationMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -486,22 +486,76 @@ public struct TargetDependencyRelationship: Serializable, Codable, Equatable, Se
}

public struct BuildOperationMetrics: Equatable, Codable, Sendable {
/// These metrics are aggregated across an entire build operation
public enum Counter: String, Equatable, Codable, Sendable {
case clangCacheHits
case clangCacheMisses
case swiftCacheHits
case swiftCacheMisses

/// How many dependencies did users declare?
case moduleDependenciesDeclared

/// How many dependencies were found to be missing?
case moduleDependenciesMissing

/// How many dependencies were found to be unused?
case moduleDependenciesUnused

/// How many dependencies warnings were emitted?
case moduleDependenciesWarningsEmitted

/// How many dependencies errors were emitted?
case moduleDependenciesErrorsEmitted

/// How many dependencies did users declare?
case headerDependenciesDeclared

/// How many dependencies were found to be missing?
case headerDependenciesMissing

/// How many dependencies were found to be unused?
case headerDependenciesUnused

/// How many dependencies warnings were emitted?
case headerDependenciesWarningsEmitted

/// How many dependencies errors were emitted?
case headerDependenciesErrorsEmitted
}

/// These metrics are aggregated by task rule info type
public enum TaskCounter: String, Equatable, Codable, Sendable {
case cacheHits
case cacheMisses

/// Number of tasks which could and did validate module dependencies
case moduleDependenciesValidatedTasks

/// Number of tasks which could but did not validate module dependencies. Together with `moduleDependenciesValidated`, this can be used to track progress towards complete validation.
case moduleDependenciesNotValidatedTasks

/// Number of unique dependencies scanned / discovered / found by an executing task
case moduleDependenciesScanned

/// Number of tasks which could and did validate header dependencies
case headerDependenciesValidatedTasks

/// Number of tasks which could but did not validate header dependencies. Together with `headerDependenciesValidated`, this can be used to track progress towards complete validation.
case headerDependenciesNotValidatedTasks

/// Number of unique dependencies scanned / discovered / found by an executing task
case headerDependenciesScanned
}

public let counters: [Counter: Int]

public init(counters: [Counter : Int]) {
/// The key is the first component of task rule info, a.k.a. the rule info type
public let taskCounters: [String: [TaskCounter: Int]]

public init(counters: [Counter : Int], taskCounters: [String: [TaskCounter: Int]]) {
self.counters = counters
self.taskCounters = taskCounters
}
}

Expand Down
Loading