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/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift new file mode 100644 index 0000000..3424439 --- /dev/null +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ArrayParsingBenchmark.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 +@_spi(RESP3) import RediStack + +func runRESP3ArrayParsing( + valueBuffer: ByteBuffer, + valueCount: Int +) throws { + let token = RESP3Token.Unchecked(buffer: valueBuffer) + + guard + case .array(let array) = try token.getValue(), + array.count == valueCount + else { + fatalError("\(#function) Test failed: Invalid test result") + } +} diff --git a/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift new file mode 100644 index 0000000..743c350 --- /dev/null +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESP3ProtocolBenchmark.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import NIOCore +import NIOEmbedded + +func runRESP3Protocol() 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") + + // 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/Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift new file mode 100644 index 0000000..7260c85 --- /dev/null +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPArrayParsingBenchmark.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +func runRESPArrayParsing( + valueBuffer: ByteBuffer, + valueCount: Int +) throws { + let translator = RESPTranslator() + 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/Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift new file mode 100644 index 0000000..74cce77 --- /dev/null +++ b/Benchmarks/Benchmarks/ProtocolBenchmark/RESPProtocolBenchmark.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import NIOCore +import NIOEmbedded + +func runRESPProtocol() 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) + 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/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 13a461c..e608e16 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( products: [ .library(name: "RediStack", targets: ["RediStack"]), .library(name: "RediStackTestUtils", targets: ["RediStackTestUtils"]), - .library(name: "RedisTypes", targets: ["RedisTypes"]) + .library(name: "RedisTypes", targets: ["RedisTypes"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"), @@ -40,7 +40,8 @@ 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"]), diff --git a/Sources/RESP3/RESP3Token.swift b/Sources/RESP3/RESP3Token.swift index 8b6dc4b..b87e197 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,96 +249,20 @@ 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 { 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? @@ -222,12 +302,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 +437,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/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