diff --git a/Sources/Benchmark/Benchmark.swift b/Sources/Benchmark/Benchmark.swift index 07c0cdf6..e2c88d08 100644 --- a/Sources/Benchmark/Benchmark.swift +++ b/Sources/Benchmark/Benchmark.swift @@ -404,6 +404,8 @@ public extension Benchmark { public var skip = false /// Customized threshold tolerances for a given metric for the Benchmark used for checking for regressions/improvements/equality. public var thresholds: [BenchmarkMetric: BenchmarkThresholds]? + /// Benchmark specific configurations to be provided to the exporters + public var exportConfigurations: BenchmarkExportConfigurations? /// Optional per-benchmark specific setup done before warmup and all iterations public var setup: BenchmarkSetupHook? /// Optional per-benchmark specific teardown done after final run is done @@ -419,6 +421,7 @@ public extension Benchmark { skip: Bool = defaultConfiguration.skip, thresholds: [BenchmarkMetric: BenchmarkThresholds]? = defaultConfiguration.thresholds, + exportConfigurations: [BenchmarkExportConfigurationKey: any BenchmarkExportConfiguration] = [:], setup: BenchmarkSetupHook? = nil, teardown: BenchmarkTeardownHook? = nil) { self.metrics = metrics @@ -430,6 +433,7 @@ public extension Benchmark { self.maxIterations = maxIterations self.skip = skip self.thresholds = thresholds + self.exportConfigurations = BenchmarkExportConfigurations(configs: exportConfigurations) self.setup = setup self.teardown = teardown } @@ -444,6 +448,7 @@ public extension Benchmark { case maxDuration case maxIterations case thresholds + case exportConfigurations } // swiftlint:enable nesting } diff --git a/Sources/Benchmark/BenchmarkExportConfigurations/BenchmarkExportConfiguration.swift b/Sources/Benchmark/BenchmarkExportConfigurations/BenchmarkExportConfiguration.swift new file mode 100644 index 00000000..7038ca9e --- /dev/null +++ b/Sources/Benchmark/BenchmarkExportConfigurations/BenchmarkExportConfiguration.swift @@ -0,0 +1,74 @@ +/// A configuration used or expected by a particular result exporter +public protocol BenchmarkExportConfiguration: Codable {} + +public struct BenchmarkExportConfigurationKey: Hashable, Codable { + private let value: String +} + +/// The set of export configurations for a particular benchmark +public struct BenchmarkExportConfigurations: Codable { + let configs: [BenchmarkExportConfigurationKey: any BenchmarkExportConfiguration] + + public init(configs: [BenchmarkExportConfigurationKey: any BenchmarkExportConfiguration]) { + self.configs = configs + } + + public subscript(_ key: BenchmarkExportConfigurationKey) -> (any BenchmarkExportConfiguration)? { + configs[key] + } +} + +extension BenchmarkExportConfigurations: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (BenchmarkExportConfigurationKey, any BenchmarkExportConfiguration)...) { + configs = Dictionary(elements, uniquingKeysWith: { $1 }) + } +} + +// N.B. We are clever with the codability implementation below +// since the value type in `BenchmarkExportConfigurations` is +// an existential type. +// The key mechanism is `BenchmarkExportConfigurationKey.resolveConfigType` +// that enables us to determine the appropriate concrete type to +// attempt to decode based on the key names located in the +// data container. + +extension BenchmarkExportConfigurationKey { + /// This is used to determine the concrete type to attempt + /// to decode for a particular ``BenchmarkExportConfigurationKey`` + static func resolveConfigType(from key: Self) -> BenchmarkExportConfiguration.Type? { + switch key { + // Add a case here when adding a new exporter config + default: nil + } + } +} + +public extension BenchmarkExportConfigurations { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: BenchmarkExportConfigurationKey.self) + self.configs = try container.allKeys.reduce( + into: [BenchmarkExportConfigurationKey: any BenchmarkExportConfiguration]() + ) { configs, key in + if let configType = type(of: key).resolveConfigType(from: key) { + configs[key] = try container.decode(configType.self, forKey: key) + } + } + } + + func encode(to encoder: any Encoder) throws { + var encoder = encoder.container(keyedBy: BenchmarkExportConfigurationKey.self) + for (key, config) in configs { + try encoder.encode(config, forKey: key) + } + } +} + +extension BenchmarkExportConfigurationKey: CodingKey { + public var stringValue: String { value } + + public init?(stringValue: String) { self.init(value: stringValue) } + + public var intValue: Int? { nil } + + public init?(intValue: Int) { nil } +}