diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..ad9a52a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,17 @@ +name: benchmark +on: + workflow_dispatch: + inputs: + sha: + type: string + required: true + description: "The commit SHA to run the benchmarks against." + push: + branches: [main] + +jobs: + benchmark: + uses: vapor/ci/.github/workflows/run-benchmark.yml@main + with: + sha: ${{ inputs.sha }} + secrets: inherit \ No newline at end of file diff --git a/Benchmarks/.gitignore b/Benchmarks/.gitignore new file mode 100644 index 0000000..6adcfa7 --- /dev/null +++ b/Benchmarks/.gitignore @@ -0,0 +1,10 @@ +.build +Packages +*.xcodeproj +Package.pins +Package.resolved +.DS_Store +DerivedData +.swiftpm +Tests/LinuxMain.swift +.vscode diff --git a/Benchmarks/.swift-format b/Benchmarks/.swift-format new file mode 120000 index 0000000..18231f5 --- /dev/null +++ b/Benchmarks/.swift-format @@ -0,0 +1 @@ +../.swift-format \ No newline at end of file diff --git a/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift b/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift new file mode 100644 index 0000000..0fd00f4 --- /dev/null +++ b/Benchmarks/ConsoleLoggerBenchmarks/ConsoleLoggerBenchmarks.swift @@ -0,0 +1,52 @@ +import Benchmark +import ConsoleKit +import ConsoleLogger +import Logging + +let benchmarks: @Sendable () -> Void = { + Benchmark.defaultConfiguration = .init( + metrics: [.peakMemoryResident, .mallocCountTotal], + thresholds: [ + .peakMemoryResident: .init( + // Tolerate up to 4% of difference compared to the threshold. + relative: [.p90: 4], + // Tolerate up to one million bytes of difference compared to the threshold. + absolute: [.p90: 1_100_000] + ), + .mallocCountTotal: .init( + // Tolerate up to 1% of difference compared to the threshold. + relative: [.p90: 1], + // Tolerate up to 2 malloc calls of difference compared to the threshold. + absolute: [.p90: 2] + ), + ] + ) + + Benchmark( + "Logging", + configuration: .init( + setup: { + let console = TestConsole() + LoggingSystem.bootstrap( + { label, provider in + ConsoleLogger(label: label, console: console) + }, + metadataProvider: .init { + ["provided1": "from metadata provider", "provided2": "another metadata provider"] + } + ) + } + ) + ) { benchmark in + var logger = Logger(label: "codes.vapor.console") + logger.logLevel = .trace + logger[metadataKey: "value"] = "one" + + for _ in benchmark.scaledIterations { + logger.info( + "Info", + metadata: ["from-log": "value", "also-from-log": "other"] + ) + } + } +} diff --git a/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift b/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift new file mode 100644 index 0000000..a7e376f --- /dev/null +++ b/Benchmarks/ConsoleLoggerBenchmarks/TestConsole.swift @@ -0,0 +1,47 @@ +import ConsoleKit +import Synchronization + +/// A test console that captures input and output for testing purposes. +/// +/// > Warning: This class is a duplicate of the one in the main package, +/// remember to update all when making changes. +final class TestConsole: Console { + let _testInputQueue: Mutex<[String]> = Mutex([]) + + var testInputQueue: [String] { + get { self._testInputQueue.withLock { $0 } } + set { self._testInputQueue.withLock { $0 = newValue } } + } + + let _testOutputQueue: Mutex<[String]> = Mutex([]) + var testOutputQueue: [String] { + get { self._testOutputQueue.withLock { $0 } } + set { self._testOutputQueue.withLock { $0 = newValue } } + } + + let _userInfo: Mutex<[AnySendableHashable: any Sendable]> = Mutex([:]) + var userInfo: [AnySendableHashable: any Sendable] { + get { self._userInfo.withLock { $0 } } + set { self._userInfo.withLock { $0 = newValue } } + } + + init() { + self.testInputQueue = [] + self.testOutputQueue = [] + self.userInfo = [:] + } + + func input(isSecure: Bool) -> String { + return testInputQueue.popLast() ?? "" + } + + func output(_ text: ConsoleText, newLine: Bool) { + testOutputQueue.insert(text.description + (newLine ? "\n" : ""), at: 0) + } + + func report(error: String, newLine: Bool) {} + + func clear(_ type: ConsoleClear) {} + + var size: (width: Int, height: Int) { (width: 32, height: 0) } +} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 0000000..780ed8c --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version:6.1 +import PackageDescription + +let package = Package( + name: "benchmarks", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(path: "../"), + .package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.29.2"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.6.3"), + ], + targets: [ + .executableTarget( + name: "ConsoleLoggerBenchmarks", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "ConsoleLogger", package: "console-kit"), + .product(name: "ConsoleKit", package: "console-kit"), + .product(name: "Logging", package: "swift-log"), + ], + path: "ConsoleLoggerBenchmarks", + swiftSettings: swiftSettings, + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark") + ] + ) + ] +) + +var swiftSettings: [SwiftSetting] { + [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("ImmutableWeakCaptures"), + ] +} diff --git a/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json new file mode 100644 index 0000000..512a674 --- /dev/null +++ b/Benchmarks/Thresholds/ConsoleLoggerBenchmarks.Logging.p90.json @@ -0,0 +1,4 @@ +{ + "mallocCountTotal" : 48, + "peakMemoryResident" : 13279231 +} \ No newline at end of file