Skip to content

Commit 9c5db40

Browse files
committed
Update RESPEncoder to write to ByteBuffer directly
This is to remove the need of allocations with Foundation.Data before writing to the buffer. We should see an estimated ~10% performance boost to encoding values.
1 parent b4fc9f3 commit 9c5db40

File tree

8 files changed

+82
-70
lines changed

8 files changed

+82
-70
lines changed

Sources/NIORedis/ChannelHandlers/RESPEncoder+MessageToByte.swift renamed to Sources/NIORedis/ChannelHandlers/RESPEncoder+M2BE.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ extension RESPEncoder: MessageToByteEncoder {
66

77
/// See `RESPEncoder.encode(data:out:)`
88
public func encode(data: RESPValue, out: inout ByteBuffer) throws {
9-
out.writeBytes(encode(data))
9+
encode(data, into: &out)
1010
}
1111
}
Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import Foundation
2-
3-
/// Translates `RedisValue` into raw bytes, formatted according to the Redis Serialization Protocol (RESP).
1+
/// Encodes `RedisValue` into a raw `ByteBuffer`, formatted according to the Redis Serialization Protocol (RESP).
42
///
5-
/// See: https://redis.io/topics/protocol
3+
/// See: [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
64
public final class RESPEncoder {
75
public init() { }
86

@@ -11,26 +9,38 @@ public final class RESPEncoder {
119
/// See https://redis.io/topics/protocol
1210
/// - Parameter value: The `RESPValue` to encode.
1311
/// - Returns: The encoded value as a collection of bytes.
14-
public func encode(_ value: RESPValue) -> Data {
12+
public func encode(_ value: RESPValue, into buffer: inout ByteBuffer) {
1513
switch value {
1614
case .simpleString(let string):
17-
return "+\(string)\r\n".convertedToData()
15+
buffer.writeStaticString("+")
16+
buffer.writeString(string)
17+
buffer.writeStaticString("\r\n")
1818

1919
case .bulkString(let data):
20-
return "$\(data.count)\r\n".convertedToData() + data + "\r\n".convertedToData()
20+
buffer.writeStaticString("$")
21+
buffer.writeString(data.count.description)
22+
buffer.writeStaticString("\r\n")
23+
buffer.writeBytes(data)
24+
buffer.writeString("\r\n")
2125

2226
case .integer(let number):
23-
return ":\(number)\r\n".convertedToData()
27+
buffer.writeStaticString(":")
28+
buffer.writeString(number.description)
29+
buffer.writeStaticString("\r\n")
2430

2531
case .null:
26-
return "$-1\r\n".convertedToData()
32+
buffer.writeStaticString("$-1\r\n")
2733

2834
case .error(let error):
29-
return "-\(error.description)\r\n".convertedToData()
35+
buffer.writeStaticString("-")
36+
buffer.writeString(error.description)
37+
buffer.writeStaticString("\r\n")
3038

3139
case .array(let array):
32-
let encodedArray = array.map(encode).reduce(into: Data(), { $0.append($1) })
33-
return "*\(array.count)\r\n".convertedToData() + encodedArray
40+
buffer.writeStaticString("*")
41+
buffer.writeString(array.count.description)
42+
buffer.writeStaticString("\r\n")
43+
array.forEach { self.encode($0, into: &buffer) }
3444
}
3545
}
3646
}

Sources/NIORedis/RESP/RESPValueConvertible.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ extension RESPValue: RESPValueConvertible {
1919
}
2020
}
2121

22+
extension RedisError: RESPValueConvertible {
23+
public init?(_ value: RESPValue) {
24+
guard let error = value.error else { return nil }
25+
self = error
26+
}
27+
28+
/// See `RESPValueConvertible.convertedToRESPValue()`
29+
public func convertedToRESPValue() -> RESPValue {
30+
return .error(self)
31+
}
32+
}
33+
2234
extension String: RESPValueConvertible {
2335
public init?(_ value: RESPValue) {
2436
guard let string = value.string else { return nil }

Tests/NIORedisTests/ChannelHandlers/RESPDecoderTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ final class RESPDecoderTests: XCTestCase {
114114
var buffer = allocator.buffer(capacity: 256)
115115
buffer.writeBytes(input)
116116
try embeddedChannel.writeInbound(buffer)
117-
return (embeddedChannel.readInbound(), embeddedChannel.readInbound())
117+
return try (embeddedChannel.readInbound(), embeddedChannel.readInbound())
118118
}
119119

120120
private func arraysAreEqual(_ lhs: [RESPValue]?, expected right: [RESPValue]) -> Bool {
@@ -183,7 +183,7 @@ extension RESPDecoderTests {
183183

184184
var results = [RESPValue?]()
185185
for _ in 0..<AllData.messages.count {
186-
results.append(embeddedChannel.readInbound())
186+
results.append(try embeddedChannel.readInbound())
187187
}
188188

189189
XCTAssertEqual(results[0]?.string, AllData.expectedString)

Tests/NIORedisTests/ChannelHandlers/RESPEncoder+ParsingTests.swift

Lines changed: 43 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,74 +7,66 @@ final class RESPEncoderParsingTests: XCTestCase {
77
private let encoder = RESPEncoder()
88

99
func testSimpleStrings() {
10-
XCTAssertEqual(
11-
encoder.encode(.simpleString("Test1")),
12-
"+Test1\r\n".convertedToData()
13-
)
14-
XCTAssertEqual(
15-
encoder.encode(.simpleString("®in§³¾")),
16-
"+®in§³¾\r\n".convertedToData()
17-
)
10+
XCTAssertTrue(testPass(input: .simpleString("Test1"), expected: "+Test1\r\n"))
11+
XCTAssertTrue(testPass(input: .simpleString("®in§³¾"), expected: "+®in§³¾\r\n"))
1812
}
1913

2014
func testBulkStrings() {
21-
let t1 = Data([0x01, 0x02, 0x0a, 0x1b, 0xaa])
22-
XCTAssertEqual(
23-
encoder.encode(.bulkString(t1)),
24-
"$5\r\n".convertedToData() + t1 + "\r\n".convertedToData()
25-
)
26-
let t2 = "®in§³¾".convertedToData()
27-
XCTAssertEqual(
28-
encoder.encode(.bulkString(t2)),
29-
"$10\r\n".convertedToData() + t2 + "\r\n".convertedToData()
30-
)
31-
let t3 = "".convertedToData()
32-
XCTAssertEqual(
33-
encoder.encode(.bulkString(t3)),
34-
"$0\r\n\r\n".convertedToData()
35-
)
15+
let bytes = Data([0x01, 0x02, 0x0a, 0x1b, 0xaa])
16+
XCTAssertTrue(testPass(input: .bulkString(bytes), expected: Data("$5\r\n".utf8) + bytes + Data("\r\n".utf8)))
17+
XCTAssertTrue(testPass(input: .init(bulk: "®in§³¾"), expected: "$10\r\n®in§³¾\r\n"))
18+
XCTAssertTrue(testPass(input: .init(bulk: ""), expected: "$0\r\n\r\n"))
3619
}
3720

3821
func testIntegers() {
39-
XCTAssertEqual(
40-
encoder.encode(.integer(Int.min)),
41-
":\(Int.min)\r\n".convertedToData()
42-
)
43-
XCTAssertEqual(
44-
encoder.encode(.integer(0)),
45-
":0\r\n".convertedToData()
46-
)
22+
XCTAssertTrue(testPass(input: .integer(Int.min), expected: ":\(Int.min)\r\n"))
23+
XCTAssertTrue(testPass(input: .integer(0), expected: ":0\r\n"))
4724
}
4825

4926
func testArrays() {
50-
XCTAssertEqual(
51-
encoder.encode(.array([])),
52-
"*0\r\n".convertedToData()
53-
)
54-
let a1: RESPValue = .array([.integer(3), .simpleString("foo")])
55-
XCTAssertEqual(
56-
encoder.encode(a1),
57-
"*2\r\n:3\r\n+foo\r\n".convertedToData()
58-
)
27+
XCTAssertTrue(testPass(input: .array([]), expected: "*0\r\n"))
28+
XCTAssertTrue(testPass(
29+
input: .array([ .integer(3), .simpleString("foo") ]),
30+
expected: "*2\r\n:3\r\n+foo\r\n"
31+
))
5932
let bytes = Data([ 0x0a, 0x1a, 0x1b, 0xff ])
60-
let a2: RESPValue = .array([.array([
61-
.integer(3),
62-
.bulkString(bytes)
63-
])])
64-
XCTAssertEqual(
65-
encoder.encode(a2),
66-
"*1\r\n*2\r\n:3\r\n$4\r\n".convertedToData() + bytes + "\r\n".convertedToData()
67-
)
33+
XCTAssertTrue(testPass(
34+
input: .array([ .array([ .integer(10), .bulkString(bytes) ]) ]),
35+
expected: Data("*1\r\n*2\r\n:10\r\n$4\r\n".utf8) + bytes + Data("\r\n".utf8)
36+
))
6837
}
6938

7039
func testError() {
7140
let error = RedisError(identifier: "testError", reason: "Manual error")
72-
let result = encoder.encode(.error(error))
73-
XCTAssertEqual(result, "-\(error.description)\r\n".convertedToData())
41+
XCTAssertTrue(testPass(input: .error(error), expected: "-\(error.description)\r\n"))
7442
}
7543

7644
func testNull() {
77-
XCTAssertEqual(encoder.encode(.null), "$-1\r\n".convertedToData())
45+
XCTAssertTrue(testPass(input: .null, expected: "$-1\r\n"))
46+
}
47+
48+
private func testPass(input: RESPValue, expected: Data) -> Bool {
49+
let allocator = ByteBufferAllocator()
50+
51+
var comparisonBuffer = allocator.buffer(capacity: expected.count)
52+
comparisonBuffer.writeBytes(expected)
53+
54+
var buffer = allocator.buffer(capacity: expected.count)
55+
encoder.encode(input.convertedToRESPValue(), into: &buffer)
56+
57+
return buffer == comparisonBuffer
58+
}
59+
60+
private func testPass(input: RESPValue, expected: String) -> Bool {
61+
let allocator = ByteBufferAllocator()
62+
63+
var comparisonBuffer = allocator.buffer(capacity: expected.count)
64+
comparisonBuffer.writeString(expected)
65+
66+
var buffer = allocator.buffer(capacity: expected.count)
67+
encoder.encode(input.convertedToRESPValue(), into: &buffer)
68+
69+
return buffer == comparisonBuffer
7870
}
7971
}
8072

Tests/NIORedisTests/ChannelHandlers/RESPEncoderTests.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ final class RESPEncoderTests: XCTestCase {
1313
encoder = RESPEncoder()
1414
allocator = ByteBufferAllocator()
1515
channel = EmbeddedChannel()
16-
_ = try? channel.pipeline.addHandler(encoder).wait()
16+
_ = try? channel.pipeline.addHandler(MessageToByteHandler(encoder)).wait()
1717
}
1818

1919
override func tearDown() {
@@ -90,10 +90,8 @@ final class RESPEncoderTests: XCTestCase {
9090
}
9191

9292
private func runEncodePass(with input: RESPValue, _ validation: (ByteBuffer) -> Void) throws {
93-
let context = try channel.pipeline.context(handler: encoder).wait()
94-
9593
var buffer = allocator.buffer(capacity: 256)
96-
try encoder.encode(context: context, data: input, out: &buffer)
94+
try encoder.encode(data: input, out: &buffer)
9795
validation(buffer)
9896
}
9997
}

0 commit comments

Comments
 (0)