diff --git a/.gitignore b/.gitignore index f6f5465..ff9a6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ DerivedData/ /Package.resolved .ci/ .docc-build/ +Package.resolved diff --git a/Benchmarks/.gitignore b/Benchmarks/.gitignore new file mode 100644 index 0000000..fd3b359 --- /dev/null +++ b/Benchmarks/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.benchmarkBaselines/ diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 0000000..4cf2b0e --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "benchmarks", + platforms: [ + .macOS(.v14), + ], + products: [ + .executable(name: "Benchmarks", targets: ["Benchmarks"]), + ], + dependencies: [ + .package(name: "swift-openapi-async-http-client", path: "../"), + .package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.26.0"), + ], + targets: [ + .executableTarget( + name: "Benchmarks", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client") + ], + path: "Sources", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark") + ] + ), + ] +) diff --git a/Benchmarks/README.md b/Benchmarks/README.md new file mode 100644 index 0000000..18cfca4 --- /dev/null +++ b/Benchmarks/README.md @@ -0,0 +1,23 @@ +# Benchmarks + +## Running + +Run and check results. + +```zsh +swift package benchmark +``` + +## Checking against baselines + +Run for a specific Swift version, for example: +```zsh +swift package benchmark baseline check --check-absolute-path Thresholds/5.10/ +``` + +## Updating baselines + +Update for a specific Swift version, for example: +```zsh +swift package --allow-writing-to-package-directory benchmark --format metricP90AbsoluteThresholds --path Thresholds/5.10/ +``` diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift new file mode 100644 index 0000000..2ac9f99 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) YEARS Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Benchmark +@_spi(Benchmarks) import OpenAPIAsyncHTTPClient +import AsyncHTTPClient +import NIOCore +import HTTPTypes +import OpenAPIRuntime +import Foundation + +let benchmarks = { + let defaultMetrics: [BenchmarkMetric] = [ + .mallocCountTotal, + .cpuTotal + ] + let config: Benchmark.Configuration = .init( + metrics: defaultMetrics, + scalingFactor: .kilo, + maxDuration: .seconds(10) + ) + + Benchmark("Creation.Default", configuration: config) { benchmark in + for _ in benchmark.scaledIterations { + blackHole({ + AsyncHTTPClientTransport() + }()) + } + } + + Benchmark("Conversion", configuration: config) { benchmark in + let request = HTTPRequest( + method: .post, + scheme: nil, + authority: nil, + path: "/stuff", + headerFields: [ + .init("x-stuff")!: "things" + ] + ) + let requestBody = HTTPBody("Hello world") + let baseURL = URL(string: "https://example.com")! + let response = HTTPClientResponse( + status: .ok, + headers: [ + "x-stuff": "things" + ], + body: .bytes(ByteBuffer(string: "Hello world")) + ) + let transport = AsyncHTTPClientTransport( + configuration: .init(), + requestSenderClosure: { _, _, _ in + response + } + ) + for _ in benchmark.scaledIterations { + let (response, responseBody) = try await transport.send( + request, + body: requestBody, + baseURL: baseURL, + operationID: "postThings" + ) + blackHole((response, responseBody)) + } + } +} diff --git a/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift b/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift index 39b79d3..84e5103 100644 --- a/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift +++ b/Sources/OpenAPIAsyncHTTPClient/AsyncHTTPClientTransport.swift @@ -127,6 +127,27 @@ public struct AsyncHTTPClientTransport: ClientTransport { self.requestSender = requestSender } + /// Creates a new transport. + /// - Parameters: + /// - configuration: A set of configuration values used by the transport. + /// - requestSenderClosure: The underlying request sender closure. + @_spi(Benchmarks) public init( + configuration: Configuration, + requestSenderClosure: @Sendable @escaping (HTTPClientRequest, HTTPClient, TimeAmount) async throws -> + HTTPClientResponse + ) { + struct ClosureRequestSender: HTTPRequestSending { + var sendClosure: + @Sendable (AsyncHTTPClientTransport.Request, HTTPClient, TimeAmount) async throws -> + AsyncHTTPClientTransport.Response + func send(request: AsyncHTTPClientTransport.Request, with client: HTTPClient, timeout: TimeAmount) + async throws -> AsyncHTTPClientTransport.Response + { try await sendClosure(request, client, timeout) } + } + self.configuration = configuration + self.requestSender = ClosureRequestSender(sendClosure: requestSenderClosure) + } + /// Creates a new transport. /// - Parameter configuration: A set of configuration values used by the transport. public init(configuration: Configuration = .init()) { diff --git a/scripts/check-license-headers.sh b/scripts/check-license-headers.sh index 9a52856..0dff43f 100644 --- a/scripts/check-license-headers.sh +++ b/scripts/check-license-headers.sh @@ -43,6 +43,9 @@ read -ra PATHS_TO_CHECK_FOR_LICENSE <<< "$( \ ":(exclude).gitignore" \ ":(exclude).spi.yml" \ ":(exclude).swift-format" \ + ":(exclude)Benchmarks/.gitignore" \ + ":(exclude)Benchmarks/Package.swift" \ + ":(exclude)Benchmarks/README.md" \ ":(exclude)CODE_OF_CONDUCT.md" \ ":(exclude)CONTRIBUTING.md" \ ":(exclude)CONTRIBUTORS.txt" \