Skip to content

Commit 8de1d14

Browse files
authored
Add ValkeyClient benchmarks (#147)
* Add ValkeyClient benchmarks Signed-off-by: Fabian Fett <[email protected]> * More Benchmarks Signed-off-by: Fabian Fett <[email protected]> * Split up benchmarks even more Signed-off-by: Fabian Fett <[email protected]> * a bit of format Signed-off-by: Fabian Fett <[email protected]> * More format Signed-off-by: Fabian Fett <[email protected]> --------- Signed-off-by: Fabian Fett <[email protected]>
1 parent 9cc7158 commit 8de1d14

File tree

6 files changed

+340
-114
lines changed

6 files changed

+340
-114
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the valkey-swift open source project
4+
//
5+
// Copyright (c) 2025 the valkey-swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of valkey-swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Benchmark
16+
import Foundation
17+
import NIOCore
18+
import NIOPosix
19+
import Valkey
20+
21+
let defaultMetrics: [BenchmarkMetric] =
22+
// There is no point comparing wallClock, cpuTotal or throughput on CI as they are too inconsistent
23+
ProcessInfo.processInfo.environment["CI"] != nil
24+
? [
25+
.instructions,
26+
.mallocCountTotal,
27+
]
28+
: [
29+
.wallClock,
30+
.cpuTotal,
31+
.instructions,
32+
.mallocCountTotal,
33+
.throughput,
34+
]
35+
36+
func makeLocalServer() async throws -> Channel {
37+
struct GetHandler: BenchmarkCommandHandler {
38+
static let expectedCommand = RESPToken.Value.bulkString(ByteBuffer(string: "GET"))
39+
static let response = ByteBuffer(string: "$3\r\nBar\r\n")
40+
func handle(command: RESPToken.Value, parameters: RESPToken.Array.Iterator, write: (ByteBuffer) -> Void) {
41+
switch command {
42+
case Self.expectedCommand:
43+
write(Self.response)
44+
case .bulkString(ByteBuffer(string: "PING")):
45+
write(ByteBuffer(string: "$4\r\nPONG\r\n"))
46+
case .bulkString(let string):
47+
fatalError("Unexpected command: \(String(buffer: string))")
48+
default:
49+
fatalError("Unexpected value: \(command)")
50+
}
51+
}
52+
}
53+
return try await ServerBootstrap(group: NIOSingletons.posixEventLoopGroup)
54+
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
55+
.childChannelInitializer { channel in
56+
do {
57+
try channel.pipeline.syncOperations.addHandler(
58+
ValkeyServerChannelHandler(commandHandler: GetHandler())
59+
)
60+
return channel.eventLoop.makeSucceededVoidFuture()
61+
} catch {
62+
return channel.eventLoop.makeFailedFuture(error)
63+
}
64+
}
65+
.bind(host: "127.0.0.1", port: 0)
66+
.get()
67+
}
68+
69+
protocol BenchmarkCommandHandler {
70+
func handle(command: RESPToken.Value, parameters: RESPToken.Array.Iterator, write: (ByteBuffer) -> Void)
71+
}
72+
73+
final class ValkeyServerChannelHandler<Handler: BenchmarkCommandHandler>: ChannelInboundHandler {
74+
75+
typealias InboundIn = ByteBuffer
76+
typealias OutboundOut = ByteBuffer
77+
78+
private var decoder = NIOSingleStepByteToMessageProcessor(RESPTokenDecoder())
79+
private let helloCommand = RESPToken.Value.bulkString(ByteBuffer(string: "HELLO"))
80+
private let helloResponse = ByteBuffer(string: "%1\r\n+server\r\n+fake\r\n")
81+
private let commandHandler: Handler
82+
83+
init(commandHandler: Handler) {
84+
self.commandHandler = commandHandler
85+
}
86+
87+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
88+
try! self.decoder.process(buffer: self.unwrapInboundIn(data)) { token in
89+
self.handleToken(context: context, token: token)
90+
}
91+
}
92+
93+
func handleToken(context: ChannelHandlerContext, token: RESPToken) {
94+
guard case .array(let array) = token.value else {
95+
fatalError()
96+
}
97+
var iterator = array.makeIterator()
98+
guard let command = iterator.next()?.value else {
99+
fatalError()
100+
}
101+
switch command {
102+
case helloCommand:
103+
context.writeAndFlush(self.wrapOutboundOut(helloResponse), promise: nil)
104+
105+
default:
106+
commandHandler.handle(command: command, parameters: iterator) {
107+
context.writeAndFlush(self.wrapOutboundOut($0), promise: nil)
108+
}
109+
}
110+
}
111+
}

Benchmarks/ValkeyBenchmarks/ValkeyBenchmarks.swift

Lines changed: 2 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -20,75 +20,10 @@ import NIOPosix
2020
import Valkey
2121

2222
let benchmarks: @Sendable () -> Void = {
23-
let defaultMetrics: [BenchmarkMetric] =
24-
// There is no point comparing wallClock, cpuTotal or throughput on CI as they are too inconsistent
25-
ProcessInfo.processInfo.environment["CI"] != nil
26-
? [
27-
.instructions,
28-
.mallocCountTotal,
29-
]
30-
: [
31-
.wallClock,
32-
.cpuTotal,
33-
.instructions,
34-
.mallocCountTotal,
35-
.throughput,
36-
]
37-
38-
var server: (any Channel)?
39-
4023
if #available(valkeySwift 1.0, *) {
41-
Benchmark("GET benchmark", configuration: .init(metrics: defaultMetrics, scalingFactor: .kilo)) { benchmark in
42-
let port = server!.localAddress!.port!
43-
let logger = Logger(label: "test")
44-
let client = ValkeyClient(.hostname("127.0.0.1", port: port), logger: logger)
45-
46-
try await withThrowingTaskGroup(of: Void.self) { group in
47-
group.addTask {
48-
await client.run()
49-
}
50-
await Task.yield()
51-
try await client.withConnection { connection in
52-
benchmark.startMeasurement()
53-
54-
for _ in benchmark.scaledIterations {
55-
let foo = try await connection.get("foo")
56-
precondition(foo.map { String(buffer: $0) } == "Bar")
57-
}
24+
connectionBenchmarks()
5825

59-
benchmark.stopMeasurement()
60-
}
61-
group.cancelAll()
62-
}
63-
} setup: {
64-
struct GetHandler: BenchmarkCommandHandler {
65-
static let expectedCommand = RESPToken.Value.bulkString(ByteBuffer(string: "GET"))
66-
static let response = ByteBuffer(string: "$3\r\nBar\r\n")
67-
func handle(command: RESPToken.Value, parameters: RESPToken.Array.Iterator, write: (ByteBuffer) -> Void) {
68-
switch command {
69-
case Self.expectedCommand:
70-
write(Self.response)
71-
default:
72-
fatalError()
73-
}
74-
}
75-
}
76-
server = try await ServerBootstrap(group: NIOSingletons.posixEventLoopGroup)
77-
.childChannelInitializer { channel in
78-
do {
79-
try channel.pipeline.syncOperations.addHandler(
80-
ValkeyServerChannelHandler(commandHandler: GetHandler())
81-
)
82-
return channel.eventLoop.makeSucceededVoidFuture()
83-
} catch {
84-
return channel.eventLoop.makeFailedFuture(error)
85-
}
86-
}
87-
.bind(host: "127.0.0.1", port: 0)
88-
.get()
89-
} teardown: {
90-
try await server?.close().get()
91-
}
26+
clientBenchmarks()
9227

9328
Benchmark("ValkeyCommandEncoder – Simple GET", configuration: .init(metrics: defaultMetrics, scalingFactor: .kilo)) { benchmark in
9429
let command = GET("foo")
@@ -143,47 +78,3 @@ let benchmarks: @Sendable () -> Void = {
14378
}
14479
}
14580
}
146-
147-
protocol BenchmarkCommandHandler {
148-
func handle(command: RESPToken.Value, parameters: RESPToken.Array.Iterator, write: (ByteBuffer) -> Void)
149-
}
150-
151-
final class ValkeyServerChannelHandler<Handler: BenchmarkCommandHandler>: ChannelInboundHandler {
152-
153-
typealias InboundIn = ByteBuffer
154-
typealias OutboundOut = ByteBuffer
155-
156-
private var decoder = NIOSingleStepByteToMessageProcessor(RESPTokenDecoder())
157-
private let helloCommand = RESPToken.Value.bulkString(ByteBuffer(string: "HELLO"))
158-
private let helloResponse = ByteBuffer(string: "%1\r\n+server\r\n+fake\r\n")
159-
private let commandHandler: Handler
160-
161-
init(commandHandler: Handler) {
162-
self.commandHandler = commandHandler
163-
}
164-
165-
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
166-
try! self.decoder.process(buffer: self.unwrapInboundIn(data)) { token in
167-
self.handleToken(context: context, token: token)
168-
}
169-
}
170-
171-
func handleToken(context: ChannelHandlerContext, token: RESPToken) {
172-
guard case .array(let array) = token.value else {
173-
fatalError()
174-
}
175-
var iterator = array.makeIterator()
176-
guard let command = iterator.next()?.value else {
177-
fatalError()
178-
}
179-
switch command {
180-
case helloCommand:
181-
context.writeAndFlush(self.wrapOutboundOut(helloResponse), promise: nil)
182-
183-
default:
184-
commandHandler.handle(command: command, parameters: iterator) {
185-
context.writeAndFlush(self.wrapOutboundOut($0), promise: nil)
186-
}
187-
}
188-
}
189-
}

0 commit comments

Comments
 (0)