From a1abe65508cd4208a97cbb2f4ab8c5ffb43a15c5 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Fri, 7 Jul 2023 10:17:55 +0200 Subject: [PATCH 1/6] Add a benchmark executable --- Package.swift | 10 ++++- .../RESP3ParsingBenchmark.swift | 22 +++++++++ .../RESP3ProtocolBenchmark.swift | 43 ++++++++++++++++++ .../RESPParsingBenchmark.swift | 18 ++++++++ .../RESPProtocolBenchmark.swift | 45 +++++++++++++++++++ Sources/RediStackPerformanceTester/main.swift | 28 ++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift create mode 100644 Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift create mode 100644 Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift create mode 100644 Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift create mode 100644 Sources/RediStackPerformanceTester/main.swift diff --git a/Package.swift b/Package.swift index 13a461c..cd61252 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,8 @@ let package = Package( products: [ .library(name: "RediStack", targets: ["RediStack"]), .library(name: "RediStackTestUtils", targets: ["RediStackTestUtils"]), - .library(name: "RedisTypes", targets: ["RedisTypes"]) + .library(name: "RedisTypes", targets: ["RedisTypes"]), + .executable(name: "RediStackPerformanceTester", targets: ["RediStackPerformanceTester"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"), @@ -44,6 +45,13 @@ let package = Package( ] ), .target(name: "RedisTypes", dependencies: ["RediStack"]), + .executableTarget( + name: "RediStackPerformanceTester", + dependencies: [ + "RediStack", + "RESP3", + ] + ), .target( name: "RediStackTestUtils", dependencies: [ diff --git a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift new file mode 100644 index 0000000..d2765e2 --- /dev/null +++ b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift @@ -0,0 +1,22 @@ +import NIOCore +import RESP3 + +func benchmarkRESP3Parsing() throws { + let valueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") + let values: [RESP3Token.Value] = [ + .blobString(ByteBuffer(string: "GET")), + .blobString(ByteBuffer(string: "welcome")), + ] + + try benchmark { + var valueBuffer = valueBuffer + + guard + let token = try RESP3Token(consuming: &valueBuffer), + case .array(let array) = token.value, + array.map(\.value) == values + else { + fatalError("\(#function) Test failed: Invalid test result") + } + } +} diff --git a/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift b/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift new file mode 100644 index 0000000..110fd2c --- /dev/null +++ b/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift @@ -0,0 +1,43 @@ +import RediStack +import RESP3 +import Foundation +import NIOCore +import NIOEmbedded + +func benchmarkRESP3Protocol() throws { + let channel = EmbeddedChannel() + + // Precalculate the server response + try channel.connect(to: .init(unixDomainSocketPath: "/fakeserver")).wait() + let serverReply = "Hello, world" + let redisReplyBuffer = ByteBuffer(string: "$\(serverReply.count)\r\n\(serverReply)\r\n") + + try benchmark { + // Client sends a command + // GET welcome + // TODO: Replace when we get RESP3 serialization + try channel.writeOutbound(ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n")) + + // Server reads the command + _ = try channel.readOutbound(as: ByteBuffer.self) + // Server replies + try channel.writeInbound(redisReplyBuffer) + + // Client reads the reply + guard var serverReplyBuffer = try channel.readInbound(as: ByteBuffer.self) else { + fatalError("Missing reply") + } + + guard case .blobString(var blobString) = try RESP3Token(consuming: &serverReplyBuffer)?.value else { + fatalError("Invalid reply") + } + + guard blobString.readString(length: blobString.readableBytes) == serverReply else { + fatalError("Invalid test result") + } + } + + guard case .clean = try channel.finish() else { + fatalError("Test didn't exit cleanly") + } +} diff --git a/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift b/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift new file mode 100644 index 0000000..c50601e --- /dev/null +++ b/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift @@ -0,0 +1,18 @@ +import NIOCore +import RediStack + +func benchmarkRESPParsing() throws { + let valueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") + let result: [RESPValue] = [ + .bulkString(ByteBuffer(string: "GET")), + .bulkString(ByteBuffer(string: "welcome")), + ] + let translator = RESPTranslator() + try benchmark { + var valueBuffer = valueBuffer + let value = try translator.parseBytes(from: &valueBuffer) + guard case .array(result) = value else { + fatalError("\(#function) Test failed: Invalid test result") + } + } +} diff --git a/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift b/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift new file mode 100644 index 0000000..65fca2e --- /dev/null +++ b/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift @@ -0,0 +1,45 @@ +import RediStack +import RESP3 +import Foundation +import NIOCore +import NIOEmbedded + +func benchmarkRESPProtocol() throws { + let channel = EmbeddedChannel() + try channel.pipeline.addBaseRedisHandlers().wait() + + // Precalculate the server response + try channel.connect(to: .init(unixDomainSocketPath: "/fakeserver")).wait() + var redisReplyBuffer = ByteBuffer() + let serverValue = "Hello, world" + let replyValue = RESPValue.simpleString(ByteBuffer(string: serverValue)) + RESPTranslator().write(replyValue, into: &redisReplyBuffer) + + try benchmark { + let promise = channel.eventLoop.makePromise(of: RESPValue.self) + + // Client sends a command + try channel.writeOutbound(RedisCommand( + message: .array([ + .bulkString(ByteBuffer(string: "GET")), + .bulkString(ByteBuffer(string: "welcome")), + ]), + responsePromise: promise + )) + + // Server reads the command + _ = try channel.readOutbound(as: ByteBuffer.self) + // Server replies + try channel.writeInbound(redisReplyBuffer) + + // Client reads the reply + let serverReply = try promise.futureResult.wait() + guard serverReply.string == serverValue else { + fatalError("Invalid test result") + } + } + + guard case .clean = try channel.finish() else { + fatalError("Test didn't exit cleanly") + } +} diff --git a/Sources/RediStackPerformanceTester/main.swift b/Sources/RediStackPerformanceTester/main.swift new file mode 100644 index 0000000..655b83a --- /dev/null +++ b/Sources/RediStackPerformanceTester/main.swift @@ -0,0 +1,28 @@ +import RediStack +import Foundation +import NIOCore +import NIOEmbedded +import RESP3 + +func benchmark(label: String = #function, _ n: Int = 100_000, run: () throws -> Void) throws { + let start = Date() + for _ in 0.. Date: Fri, 7 Jul 2023 10:28:26 +0200 Subject: [PATCH 2/6] Add license headers --- .../RESP3ParsingBenchmark.swift | 16 +++++++++++++++- .../RESP3ProtocolBenchmark.swift | 14 ++++++++++++++ .../RESPParsingBenchmark.swift | 14 ++++++++++++++ .../RESPProtocolBenchmark.swift | 14 ++++++++++++++ Sources/RediStackPerformanceTester/main.swift | 14 ++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift index d2765e2..78ec90a 100644 --- a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift +++ b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import NIOCore import RESP3 @@ -7,7 +21,7 @@ func benchmarkRESP3Parsing() throws { .blobString(ByteBuffer(string: "GET")), .blobString(ByteBuffer(string: "welcome")), ] - + try benchmark { var valueBuffer = valueBuffer diff --git a/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift b/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift index 110fd2c..bce4d5f 100644 --- a/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift +++ b/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import RediStack import RESP3 import Foundation diff --git a/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift b/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift index c50601e..881fe8a 100644 --- a/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift +++ b/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import NIOCore import RediStack diff --git a/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift b/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift index 65fca2e..db13908 100644 --- a/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift +++ b/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import RediStack import RESP3 import Foundation diff --git a/Sources/RediStackPerformanceTester/main.swift b/Sources/RediStackPerformanceTester/main.swift index 655b83a..0acd206 100644 --- a/Sources/RediStackPerformanceTester/main.swift +++ b/Sources/RediStackPerformanceTester/main.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import RediStack import Foundation import NIOCore From 600d23947c5b7fe6bd102175e221203cf8c557b4 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Fri, 7 Jul 2023 16:05:15 +0200 Subject: [PATCH 3/6] Remove RediStackPerformanceTester from the package's products --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index cd61252..1487a9d 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,6 @@ let package = Package( .library(name: "RediStack", targets: ["RediStack"]), .library(name: "RediStackTestUtils", targets: ["RediStackTestUtils"]), .library(name: "RedisTypes", targets: ["RedisTypes"]), - .executable(name: "RediStackPerformanceTester", targets: ["RediStackPerformanceTester"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"), From f38417136cea7f8890e988a77c92fb36c4ddb286 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 22 Oct 2023 00:48:47 +0200 Subject: [PATCH 4/6] Add a variant of RESP3Token that does not do validation, while providing errors when unwrapping the value --- Sources/RESP3/RESP3Token.swift | 250 +++++++++++------- .../RESP3ParsingBenchmark.swift | 8 +- 2 files changed, 165 insertions(+), 93 deletions(-) diff --git a/Sources/RESP3/RESP3Token.swift b/Sources/RESP3/RESP3Token.swift index 8b6dc4b..41affee 100644 --- a/Sources/RESP3/RESP3Token.swift +++ b/Sources/RESP3/RESP3Token.swift @@ -15,6 +15,162 @@ import NIOCore public struct RESP3Token: Hashable, Sendable { + public struct Unchecked: Hashable, Sendable { + let base: ByteBuffer + + public init(buffer: ByteBuffer) { + self.base = buffer + } + + public func getValue() throws -> Value { + var local = self.base + + switch local.readValidatedRESP3TypeIdentifier() { + case .null: + return .null + + case .boolean: + switch local.readInteger(as: UInt8.self) { + case UInt8.t: + return .boolean(true) + case UInt8.f: + return .boolean(false) + default: + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + case .blobString: + guard + var lengthSlice = try local.readCRLFTerminatedSlice2(), + let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes), + let length = Int(lengthString), + let string = local.readSlice(length: length) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .blobString(string) + + case .blobError: + guard + var lengthSlice = try local.readCRLFTerminatedSlice2(), + let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes), + let length = Int(lengthString), + let slice = local.readSlice(length: length) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .blobError(slice) + + case .simpleString: + guard let slice = try local.readCRLFTerminatedSlice2() else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .simpleString(slice) + + case .simpleError: + guard let slice = try local.readCRLFTerminatedSlice2() else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .simpleError(slice) + + case .array: + guard + var countSlice = try local.readCRLFTerminatedSlice2(), + let countString = countSlice.readString(length: countSlice.readableBytes), + let count = Int(countString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .array(.init(count: count, buffer: local)) + + case .push: + guard + var countSlice = try local.readCRLFTerminatedSlice2(), + let countString = countSlice.readString(length: countSlice.readableBytes), + let count = Int(countString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .push(.init(count: count, buffer: local)) + + case .set: + guard + var countSlice = try local.readCRLFTerminatedSlice2(), + let countString = countSlice.readString(length: countSlice.readableBytes), + let count = Int(countString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .set(.init(count: count, buffer: local)) + + case .attribute: + guard + var countSlice = try local.readCRLFTerminatedSlice2(), + let countString = countSlice.readString(length: countSlice.readableBytes), + let count = Int(countString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .attribute(.init(count: count, buffer: local)) + + case .map: + guard + var countSlice = try local.readCRLFTerminatedSlice2(), + let countString = countSlice.readString(length: countSlice.readableBytes), + let count = Int(countString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .map(.init(count: count, buffer: local)) + + case .integer: + var numberSlice = try local.readCRLFTerminatedSlice2()! + let numberString = numberSlice.readString(length: numberSlice.readableBytes)! + let number = Int64(numberString)! + return .number(number) + + case .double: + guard + var numberSlice = try local.readCRLFTerminatedSlice2(), + let numberString = numberSlice.readString(length: numberSlice.readableBytes), + let number = Double(numberString) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .double(number) + + case .verbatimString: + guard + var lengthSlice = try! local.readCRLFTerminatedSlice2(), + let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes), + let length = Int(lengthString), + let slice = local.readSlice(length: length) + else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .verbatimString(slice) + + case .bigNumber: + guard let lengthSlice = try local.readCRLFTerminatedSlice2() else { + throw RESP3ParsingError(code: .invalidData, buffer: base) + } + + return .bigNumber(lengthSlice) + } + } + } + public struct Array: Sequence, Sendable, Hashable { public typealias Element = RESP3Token @@ -93,90 +249,10 @@ public struct RESP3Token: Hashable, Sendable { case push(Array) } - let base: ByteBuffer + let wrapped: Unchecked public var value: Value { - var local = self.base - - switch local.readValidatedRESP3TypeIdentifier() { - case .null: - return .null - - case .boolean: - return .boolean(local.readInteger(as: UInt8.self)! == .t) - - case .blobString: - var lengthSlice = try! local.readCRLFTerminatedSlice2()! - let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)! - let length = Int(lengthString)! - return .blobString(local.readSlice(length: length)!) - - case .blobError: - var lengthSlice = try! local.readCRLFTerminatedSlice2()! - let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)! - let length = Int(lengthString)! - return .blobError(local.readSlice(length: length)!) - - case .simpleString: - let slice = try! local.readCRLFTerminatedSlice2()! - return .simpleString(slice) - - case .simpleError: - let slice = try! local.readCRLFTerminatedSlice2()! - return .simpleError(slice) - - case .array: - var countSlice = try! local.readCRLFTerminatedSlice2()! - let countString = countSlice.readString(length: countSlice.readableBytes)! - let count = Int(countString)! - return .array(.init(count: count, buffer: local)) - - case .push: - var countSlice = try! local.readCRLFTerminatedSlice2()! - let countString = countSlice.readString(length: countSlice.readableBytes)! - let count = Int(countString)! - return .push(.init(count: count, buffer: local)) - - case .set: - var countSlice = try! local.readCRLFTerminatedSlice2()! - let countString = countSlice.readString(length: countSlice.readableBytes)! - let count = Int(countString)! - return .set(.init(count: count, buffer: local)) - - case .attribute: - var countSlice = try! local.readCRLFTerminatedSlice2()! - let countString = countSlice.readString(length: countSlice.readableBytes)! - let count = Int(countString)! - return .attribute(.init(count: count, buffer: local)) - - case .map: - var countSlice = try! local.readCRLFTerminatedSlice2()! - let countString = countSlice.readString(length: countSlice.readableBytes)! - let count = Int(countString)! - return .map(.init(count: count, buffer: local)) - - case .integer: - var numberSlice = try! local.readCRLFTerminatedSlice2()! - let numberString = numberSlice.readString(length: numberSlice.readableBytes)! - let number = Int64(numberString)! - return .number(number) - - case .double: - var numberSlice = try! local.readCRLFTerminatedSlice2()! - let numberString = numberSlice.readString(length: numberSlice.readableBytes)! - let number = Double(numberString)! - return .double(number) - - case .verbatimString: - var lengthSlice = try! local.readCRLFTerminatedSlice2()! - let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)! - let length = Int(lengthString)! - return .verbatimString(local.readSlice(length: length)!) - - case .bigNumber: - let lengthSlice = try! local.readCRLFTerminatedSlice2()! - return .bigNumber(lengthSlice) - } + try! wrapped.getValue() } public init?(consuming buffer: inout ByteBuffer) throws { @@ -222,12 +298,8 @@ public struct RESP3Token: Hashable, Sendable { return nil } - guard let validated = validated else { return nil } - self.base = validated - } - - init(validated: ByteBuffer) { - self.base = validated + guard let validated else { return nil } + self.wrapped = Unchecked(buffer: validated) } } @@ -361,7 +433,7 @@ extension ByteBuffer { guard let new = try RESP3Token(consuming: &localCopy, depth: depth + 1) else { return nil } - bodyLength += new.base.readableBytes + bodyLength += new.wrapped.base.readableBytes } return bodyLength } diff --git a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift index 78ec90a..cf30788 100644 --- a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift +++ b/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift @@ -21,14 +21,14 @@ func benchmarkRESP3Parsing() throws { .blobString(ByteBuffer(string: "GET")), .blobString(ByteBuffer(string: "welcome")), ] + let token = RESP3Token.Unchecked(buffer: valueBuffer) try benchmark { - var valueBuffer = valueBuffer + let token = token guard - let token = try RESP3Token(consuming: &valueBuffer), - case .array(let array) = token.value, - array.map(\.value) == values + case .array(let array) = try token.getValue(), + array.count == values.count else { fatalError("\(#function) Test failed: Invalid test result") } From 6ca5f57e6cc89c557d27bfb753c4a67869e263ab Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 22 Oct 2023 22:25:44 +0200 Subject: [PATCH 5/6] Fix the unit tests --- Sources/RESP3/RESP3Token.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/RESP3/RESP3Token.swift b/Sources/RESP3/RESP3Token.swift index 41affee..b87e197 100644 --- a/Sources/RESP3/RESP3Token.swift +++ b/Sources/RESP3/RESP3Token.swift @@ -259,6 +259,10 @@ public struct RESP3Token: Hashable, Sendable { try self.init(consuming: &buffer, depth: 0) } + public init(validated buffer: ByteBuffer) { + self.wrapped = .init(buffer: buffer) + } + fileprivate init?(consuming buffer: inout ByteBuffer, depth: Int) throws { let validated: ByteBuffer? From 4f6ae6f0d158ed78e66dabd8b1b4412d8309e26f Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sat, 16 Dec 2023 11:54:53 +0100 Subject: [PATCH 6/6] Add benchmarks based on ordo-one's package-benchmark --- .../ProtocolBenchmark/ProtocolBenchmark.swift | 46 ++++++++++++++++++ .../RESP3ArrayParsingBenchmark.swift | 26 ++++------ .../RESP3ProtocolBenchmark.swift | 46 +++++++++--------- .../RESPArrayParsingBenchmark.swift | 20 ++++---- .../RESPProtocolBenchmark.swift | 48 +++++++++---------- Benchmarks/Package.swift | 40 ++++++++++++++++ Package.swift | 10 +--- Sources/RediStack/ExportForBenchmark.swift | 1 + Sources/RediStackPerformanceTester/main.swift | 42 ---------------- 9 files changed, 151 insertions(+), 128 deletions(-) create mode 100644 Benchmarks/Benchmarks/ProtocolBenchmark/ProtocolBenchmark.swift rename Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift => Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift (51%) rename {Sources/RediStackPerformanceTester => Benchmarks/Benchmarks/ProtocolBenchmark}/RESP3ProtocolBenchmark.swift (50%) rename Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift => Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift (52%) rename {Sources/RediStackPerformanceTester => Benchmarks/Benchmarks/ProtocolBenchmark}/RESPProtocolBenchmark.swift (56%) create mode 100644 Benchmarks/Package.swift create mode 100644 Sources/RediStack/ExportForBenchmark.swift delete mode 100644 Sources/RediStackPerformanceTester/main.swift diff --git a/Benchmarks/Benchmarks/ProtocolBenchmark/ProtocolBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/ProtocolBenchmark.swift new file mode 100644 index 0000000..e47eb9c --- /dev/null +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/ProtocolBenchmark.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(RESP3) import RediStack +import Benchmark +import NIOCore + +let benchmarks = { + let resp3ArrayValueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") + let resp3ArrayCount = 2 + + Benchmark("RESP3 Array Parsing") { benchmark in + try runRESP3ArrayParsing( + valueBuffer: resp3ArrayValueBuffer, + valueCount: resp3ArrayCount + ) + } + + let respArrayValueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") + let respArrayCount = 2 + Benchmark("RESP Array Parsing") { benchmark in + try runRESPArrayParsing( + valueBuffer: respArrayValueBuffer, + valueCount: respArrayCount + ) + } + + Benchmark("RESP3 Conversation") { benchmark in + try runRESP3Protocol() + } + + Benchmark("RESP Conversation") { benchmark in + try runRESPProtocol() + } +} diff --git a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift similarity index 51% rename from Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift rename to Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift index cf30788..3424439 100644 --- a/Sources/RediStackPerformanceTester/RESP3ParsingBenchmark.swift +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift @@ -13,24 +13,18 @@ //===----------------------------------------------------------------------===// import NIOCore -import RESP3 +@_spi(RESP3) import RediStack -func benchmarkRESP3Parsing() throws { - let valueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") - let values: [RESP3Token.Value] = [ - .blobString(ByteBuffer(string: "GET")), - .blobString(ByteBuffer(string: "welcome")), - ] +func runRESP3ArrayParsing( + valueBuffer: ByteBuffer, + valueCount: Int +) throws { let token = RESP3Token.Unchecked(buffer: valueBuffer) - try benchmark { - let token = token - - guard - case .array(let array) = try token.getValue(), - array.count == values.count - else { - fatalError("\(#function) Test failed: Invalid test result") - } + guard + case .array(let array) = try token.getValue(), + array.count == valueCount + else { + fatalError("\(#function) Test failed: Invalid test result") } } diff --git a/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift similarity index 50% rename from Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift rename to Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift index bce4d5f..743c350 100644 --- a/Sources/RediStackPerformanceTester/RESP3ProtocolBenchmark.swift +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift @@ -18,7 +18,7 @@ import Foundation import NIOCore import NIOEmbedded -func benchmarkRESP3Protocol() throws { +func runRESP3Protocol() throws { let channel = EmbeddedChannel() // Precalculate the server response @@ -26,29 +26,27 @@ func benchmarkRESP3Protocol() throws { let serverReply = "Hello, world" let redisReplyBuffer = ByteBuffer(string: "$\(serverReply.count)\r\n\(serverReply)\r\n") - try benchmark { - // Client sends a command - // GET welcome - // TODO: Replace when we get RESP3 serialization - try channel.writeOutbound(ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n")) - - // Server reads the command - _ = try channel.readOutbound(as: ByteBuffer.self) - // Server replies - try channel.writeInbound(redisReplyBuffer) - - // Client reads the reply - guard var serverReplyBuffer = try channel.readInbound(as: ByteBuffer.self) else { - fatalError("Missing reply") - } - - guard case .blobString(var blobString) = try RESP3Token(consuming: &serverReplyBuffer)?.value else { - fatalError("Invalid reply") - } - - guard blobString.readString(length: blobString.readableBytes) == serverReply else { - fatalError("Invalid test result") - } + // Client sends a command + // GET welcome + // TODO: Replace when we get RESP3 serialization + try channel.writeOutbound(ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n")) + + // Server reads the command + _ = try channel.readOutbound(as: ByteBuffer.self) + // Server replies + try channel.writeInbound(redisReplyBuffer) + + // Client reads the reply + guard var serverReplyBuffer = try channel.readInbound(as: ByteBuffer.self) else { + fatalError("Missing reply") + } + + guard case .blobString(var blobString) = try RESP3Token(consuming: &serverReplyBuffer)?.value else { + fatalError("Invalid reply") + } + + guard blobString.readString(length: blobString.readableBytes) == serverReply else { + fatalError("Invalid test result") } guard case .clean = try channel.finish() else { diff --git a/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift similarity index 52% rename from Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift rename to Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift index 881fe8a..7260c85 100644 --- a/Sources/RediStackPerformanceTester/RESPParsingBenchmark.swift +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift @@ -15,18 +15,14 @@ import NIOCore import RediStack -func benchmarkRESPParsing() throws { - let valueBuffer = ByteBuffer(string: "*2\r\n$3\r\nGET\r\n$7\r\nwelcome\r\n") - let result: [RESPValue] = [ - .bulkString(ByteBuffer(string: "GET")), - .bulkString(ByteBuffer(string: "welcome")), - ] +func runRESPArrayParsing( + valueBuffer: ByteBuffer, + valueCount: Int +) throws { let translator = RESPTranslator() - try benchmark { - var valueBuffer = valueBuffer - let value = try translator.parseBytes(from: &valueBuffer) - guard case .array(result) = value else { - fatalError("\(#function) Test failed: Invalid test result") - } + var valueBuffer = valueBuffer + let value = try translator.parseBytes(from: &valueBuffer) + guard case .array(let result) = value, result.count == valueCount else { + fatalError("\(#function) Test failed: Invalid test result") } } diff --git a/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift similarity index 56% rename from Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift rename to Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift index db13908..74cce77 100644 --- a/Sources/RediStackPerformanceTester/RESPProtocolBenchmark.swift +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift @@ -12,13 +12,12 @@ // //===----------------------------------------------------------------------===// -import RediStack -import RESP3 +@_spi(RESP3) import RediStack import Foundation import NIOCore import NIOEmbedded -func benchmarkRESPProtocol() throws { +func runRESPProtocol() throws { let channel = EmbeddedChannel() try channel.pipeline.addBaseRedisHandlers().wait() @@ -28,29 +27,26 @@ func benchmarkRESPProtocol() throws { let serverValue = "Hello, world" let replyValue = RESPValue.simpleString(ByteBuffer(string: serverValue)) RESPTranslator().write(replyValue, into: &redisReplyBuffer) - - try benchmark { - let promise = channel.eventLoop.makePromise(of: RESPValue.self) - - // Client sends a command - try channel.writeOutbound(RedisCommand( - message: .array([ - .bulkString(ByteBuffer(string: "GET")), - .bulkString(ByteBuffer(string: "welcome")), - ]), - responsePromise: promise - )) - - // Server reads the command - _ = try channel.readOutbound(as: ByteBuffer.self) - // Server replies - try channel.writeInbound(redisReplyBuffer) - - // Client reads the reply - let serverReply = try promise.futureResult.wait() - guard serverReply.string == serverValue else { - fatalError("Invalid test result") - } + let promise = channel.eventLoop.makePromise(of: RESPValue.self) + + // Client sends a command + try channel.writeOutbound(RedisCommand( + message: .array([ + .bulkString(ByteBuffer(string: "GET")), + .bulkString(ByteBuffer(string: "welcome")), + ]), + responsePromise: promise + )) + + // Server reads the command + _ = try channel.readOutbound(as: ByteBuffer.self) + // Server replies + try channel.writeInbound(redisReplyBuffer) + + // Client reads the reply + let serverReply = try promise.futureResult.wait() + guard serverReply.string == serverValue else { + fatalError("Invalid test result") } guard case .clean = try channel.finish() else { diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 0000000..43ef813 --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.9 +//===----------------------------------------------------------------------===// +// +// This source file is part of the RediStack open source project +// +// Copyright (c) 2019-2023 RediStack project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of RediStack project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "Benchmarks", + platforms: [ + .macOS(.v13), + ], + dependencies: [ + .package(path: "../"), + .package(url: "https://github.com/ordo-one/package-benchmark.git", exact: "1.11.1"), + ], + targets: [ + .executableTarget( + name: "ProtocolBenchmark", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "RediStack", package: "RediStack"), + ], + path: "Benchmarks/ProtocolBenchmark", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark") + ] + ), + ] +) \ No newline at end of file diff --git a/Package.swift b/Package.swift index 1487a9d..e608e16 100644 --- a/Package.swift +++ b/Package.swift @@ -40,17 +40,11 @@ let package = Package( .product(name: "NIOSSL", package: "swift-nio-ssl"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Logging", package: "swift-log"), - .product(name: "Metrics", package: "swift-metrics") + .product(name: "Metrics", package: "swift-metrics"), + .target(name: "RESP3"), ] ), .target(name: "RedisTypes", dependencies: ["RediStack"]), - .executableTarget( - name: "RediStackPerformanceTester", - dependencies: [ - "RediStack", - "RESP3", - ] - ), .target( name: "RediStackTestUtils", dependencies: [ diff --git a/Sources/RediStack/ExportForBenchmark.swift b/Sources/RediStack/ExportForBenchmark.swift new file mode 100644 index 0000000..0f0ef03 --- /dev/null +++ b/Sources/RediStack/ExportForBenchmark.swift @@ -0,0 +1 @@ +@_spi(RESP3) @_exported import RESP3 \ No newline at end of file diff --git a/Sources/RediStackPerformanceTester/main.swift b/Sources/RediStackPerformanceTester/main.swift deleted file mode 100644 index 0acd206..0000000 --- a/Sources/RediStackPerformanceTester/main.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the RediStack open source project -// -// Copyright (c) 2023 RediStack project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of RediStack project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import RediStack -import Foundation -import NIOCore -import NIOEmbedded -import RESP3 - -func benchmark(label: String = #function, _ n: Int = 100_000, run: () throws -> Void) throws { - let start = Date() - for _ in 0..