diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c108955..5eefb02c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 15 strategy: matrix: - image: ["swift:6.0", "swift:6.1"] + image: ["swift:6.0", "swift:6.1", "swiftlang/swift:nightly-main-jammy"] container: image: ${{ matrix.image }} services: @@ -31,6 +31,10 @@ jobs: - 6379:6379 options: --entrypoint valkey-server steps: + - name: Install jemalloc + run: | + apt-get update + apt-get install -y libjemalloc-dev - name: Checkout uses: actions/checkout@v4 - name: Test diff --git a/Benchmarks/ValkeyBenchmarks/ValkeyBenchmarks.swift b/Benchmarks/ValkeyBenchmarks/ValkeyBenchmarks.swift new file mode 100644 index 00000000..2a2f2830 --- /dev/null +++ b/Benchmarks/ValkeyBenchmarks/ValkeyBenchmarks.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the swift-valkey open source project +// +// Copyright (c) 2025 Apple Inc. and the swift-valkey project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of swift-valkey project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Benchmark +import Logging +import NIOCore +import NIOPosix +import Valkey + +let benchmarks : @Sendable () -> Void = { + let defaultMetrics: [BenchmarkMetric] = [ + .wallClock, + .cpuTotal, + .mallocCountTotal, + .throughput, + .instructions, + ] + + var server: Channel? + Benchmark("GET benchmark", configuration: .init(metrics: defaultMetrics, scalingFactor: .kilo)) { benchmark in + let port = server!.localAddress!.port! + let logger = Logger(label: "test") + let client = ValkeyClient(.hostname("127.0.0.1", port: port), logger: logger) + + try await client.withConnection(logger: logger) { connection in + benchmark.startMeasurement() + + for _ in benchmark.scaledIterations { + let foo = try await connection.get(key: "foo") + precondition(foo == "Bar") + } + + benchmark.stopMeasurement() + } + } setup: { + server = try await ServerBootstrap(group: NIOSingletons.posixEventLoopGroup) + .childChannelInitializer { channel in + do { + try channel.pipeline.syncOperations.addHandler(ValkeyServerChannelHandler()) + return channel.eventLoop.makeSucceededVoidFuture() + } catch { + return channel.eventLoop.makeFailedFuture(error) + } + } + .bind(host: "127.0.0.1", port: 0) + .get() + } teardown: { + try await server?.close().get() + } +} + +final class ValkeyServerChannelHandler: ChannelInboundHandler { + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + + private var decoder = NIOSingleStepByteToMessageProcessor(RESPTokenDecoder()) + private let response = ByteBuffer(string: "$3\r\nBar\r\n") + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + try! self.decoder.process(buffer: self.unwrapInboundIn(data)) { token in + self.handleToken(context: context, token: token) + } + } + + func handleToken(context: ChannelHandlerContext, token: RESPToken) { + guard case .array(let array) = token.value else { + fatalError() + } + + var iterator = array.makeIterator() + switch iterator.next()?.value { + case .bulkString(ByteBuffer(string: "HELLO")): + let map = "%1\r\n+server\r\n+fake\r\n" + context.writeAndFlush(self.wrapOutboundOut(ByteBuffer(string: map)), promise: nil) + + case .bulkString(ByteBuffer(string: "GET")): + context.writeAndFlush(self.wrapOutboundOut(self.response), promise: nil) + + default: + fatalError() + } + } +} diff --git a/Package.swift b/Package.swift index 575d3cdc..370798e0 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.79.0"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.29.0"), .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.23.0"), + + .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.29.2"), ], targets: [ .target( @@ -32,6 +34,20 @@ let package = Package( name: "ValkeyCommandsBuilder", resources: [.process("Resources")] ), + .executableTarget( + name: "ValkeyBenchmarks", + dependencies: [ + "Valkey", + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ], + path: "Benchmarks/ValkeyBenchmarks", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark"), + ] + ), .testTarget( name: "IntegrationTests", dependencies: ["Valkey"] diff --git a/Sources/Valkey/ValkeyClient.swift b/Sources/Valkey/ValkeyClient.swift index c812d3f9..1b92fb1c 100644 --- a/Sources/Valkey/ValkeyClient.swift +++ b/Sources/Valkey/ValkeyClient.swift @@ -62,7 +62,7 @@ extension ValkeyClient { /// - operation: Closure handling Valkey connection public func withConnection( logger: Logger, - operation: @escaping @Sendable (ValkeyConnection) async throws -> Value + operation: (ValkeyConnection) async throws -> Value ) async throws -> Value { let valkeyConnection = try await ValkeyConnection.connect( address: self.serverAddress,