Skip to content

Commit 07db049

Browse files
authored
Merge pull request #15 from HebiRobotics/develop
performance / json / string improvements
2 parents 1e4f044 + 57c3285 commit 07db049

50 files changed

Lines changed: 2703 additions & 881 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ msg.getMutableNestedMessage().setPrimitiveValue(0);
231231
<details>
232232
<summary>String Fields</summary><p>
233233

234-
Java `String` objects are immutable, so the API differs from Protobuf-Java in that accessors accept `CharSequence` arguments and return `StringBuilder` objects instead. `StringBuilder` can be converted via `toString()`, but you may want to use a `StringInterner` to share references if you receive many identical strings.
234+
`String` types are internally stored as `Utf8String` that are lazily parsed and can be set with `CharSequence`. Since Java `String` objects are immutable, there are additional access methods to allow for decoding characters into a reusable `StringBuilder` instance, as well as for using a custom `Utf8Decoder` that can implement interning.
235235

236236
```proto
237237
// .proto
@@ -243,22 +243,24 @@ message SimpleMessage {
243243
```Java
244244
// simplified generated code
245245
public final class SimpleMessage {
246-
public SimpleMessage setOptionalString(CharSequence value); // copies data
246+
public SimpleMessage setOptionalString(CharSequence value);
247247
public SimpleMessage clearOptionalString(); // sets length = 0
248248
public boolean hasOptionalString();
249-
public StringBuilder getOptionalString(); // internal store -> treat as read-only
250-
public StringBuilder getMutableOptionalString(); // internal store -> may be modified
249+
public String getOptionalString(); // lazily converted string
250+
public Utf8String getOptionalStringBytes(); // internal representation -> treat as read-only
251+
public Utf8String getMutableOptionalStringBytes(); // internal representation -> may be modified until has state is cleared
251252

252253
private final StringBuilder optionalString = new StringBuilder(0);
253254
}
254255
```
255256

256257
```Java
257-
// Set and append to a string field
258-
SimpleMessage msg = SimpleMessage.newInstance();
259-
msg.setOptionalString("my-");
260-
msg.getMutableOptionalString()
261-
.append("text"); // field is now 'my-text'
258+
// Get characters
259+
SimpleMessage msg = SimpleMessage.newInstance()
260+
.setOptionalString("my-text");
261+
262+
StringBuilder chars = new StringBuilder();
263+
msg.getOptionalStringBytes().getChars(chars); // chars now contains "my-text"
262264
```
263265

264266
</p></details>

benchmarks/README.md

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,47 @@ The first benchmark was copied from [Small Binary Encoding's](https://mechanical
1111

1212
| Test [msg/ms] | QuickBuffers | Protobuf-Java | Ratio
1313
| :----------- | :-----------: | :-----------: | :-----------: |
14-
| Car Encode | 2808 (375 MB/s) | 985 (132 MB/s) | 2.9
15-
| Car Decode | 2266 (303 MB/s) | 1271 (170 MB/s) | 1.8
16-
| Market Data Encode | 8163 (498 MB/s) | 3700 (226 MB/s) | 2.2
17-
| Market Data Decode | 7535 (460 MB/s) | 3306 (202 MB/s) | 2.3
14+
| Car Encode | 3649 (487 MB/s) | 985 (132 MB/s) | 3.7
15+
| Car Decode | 2329 (311 MB/s) | 1271 (170 MB/s) | 1.8
16+
| Market Data Encode | 13177 (804 MB/s) | 3700 (226 MB/s) | 3.6
17+
| Market Data Decode | 9805 (598 MB/s) | 3306 (202 MB/s) | 3.0
1818

19-
Note that this test was done using the original SBE .proto definitions. If the varint types are changed to a less expensive encoding, e.g., `fixed64/32` instead of `int64/32`, the market data numbers improve by another 10-20%. By additionally inlining the small nested fields it'd result in 3-4x the original message throughput of Protobuf-Java. The choice of type can have a huge impact on the performance.
19+
Note that this test was done using the original SBE .proto definitions. If the varint types are changed to a less expensive encoding, e.g., `fixed64/32` instead of `int64/32`, the results improve by 30-50%. By additionally inlining the small nested fields it'd result in more than 5x the original message throughput. Overall, be aware that there is a significant trade-off between wire size and encoding speed.
2020

21-
We also compared the built-in JSON encoding and found that for this particular benchmark the message throughput is roughly the same as Protobuf-Java. However, at 559 byte (car) and 435 byte (market) the uncompressed binary sizes are significantly larger.
21+
We also compared the built-in JSON encoding and found that for this particular benchmark the message throughput is on par with Protobuf-Java. However, at 559 byte (car) and 435 byte (market) the uncompressed binary sizes are significantly larger.
2222

2323
<!-- car mutliplier: 559 * 1000 / (1024*1024) = 0.5331 = -->
2424
<!-- market multiplier: 435 * 1000 / (1024*1024) = 0.415 = -->
2525

2626
| Test [msg/ms] | QuickBuffers (JSON) | Protobuf-Java (Binary) | Ratio
2727
| :----------- | :-----------: | :-----------: | :-----------: |
28-
| Car Encode | 1515 (808 MB/s) | 985 | 1.5
29-
| Market Data Encode | 3338 (1.4 GB/s) | 3700 | 0.9
28+
| Car Encode | 1599 (852 MB/s) | 985 | 1.6
29+
| Market Data Encode | 3691 (1.5 GB/s) | 3700 | 1.0
3030

3131
## Benchmark 2 - File Streams
3232

33-
We also ran benchmarks for reading and writing streams of delimited protobuf messages with varying contents, which is similar to reading sequentially from a log file. All datasets were loaded into memory and decoded from a byte array. Neither benchmark triggers Protobuf-Java's lazy-parsing of strings, so the results may be slightly off. The benchmark code can be found in the `benchmarks` directory.
33+
We also ran benchmarks for reading and writing streams of delimited protobuf messages with varying contents, which is similar to reading sequentially from a log file. All datasets were loaded into memory and decoded from a byte array. This benchmark does not trigger lazy-parsing of strings, so it is primarily indicative of forwarding use cases. The benchmark code can be found in the `benchmarks` directory.
3434

3535
| | QuickBuffers<p>(Unsafe) | QuickBuffers<p>(without Unsafe) | Java`[1]`| JavaLite`[1]` | `[2]`
3636
| ----------- | -----------: | -----------: | -----------: | -----------: | ----------- |
3737
| **Read** | |
38-
| 1 | 173ms (502 MB/s) | 212ms (410 MB/s) | 344ms (253 MB/s) | 567ms (153 MB/s) | 2.0
39-
| 2 | 102ms (559 MB/s)` | 118ms (483 MB/s) | 169ms (337 MB/s) | 378ms (150 MB/s) | 1.7
40-
| 3 | 34ms (297 MB/s) | 44ms (226 MB/s) | 65ms (153 MB/s) | 147ms (68 MB/s) | 1.9
41-
| 4 | 25ms (400 MB/s) | 28ms (353 MB/s) | 47ms (214 MB/s) | 155ms (65 MB/s) | 1.9
42-
| 5 | 9.8ms (6.5 GB/s) | 44ms (1.5 GB/s) | 103ms (621 MB/s) | 92ms (696 MB/s) | 10.5
38+
| 1 | 144ms (604 MB/s) | 149ms (584 MB/s) | 344ms (253 MB/s) | 567ms (153 MB/s) | 2.4
39+
| 2 | 79ms (722 MB/s)` | 90ms (633 MB/s) | 169ms (337 MB/s) | 378ms (150 MB/s) | 2.1
40+
| 3 | 30ms (333 MB/s) | 35ms (286 MB/s) | 65ms (153 MB/s) | 147ms (68 MB/s) | 2.2
41+
| 4 | 21ms (476 MB/s) | 21ms (476 MB/s) | 47ms (214 MB/s) | 155ms (65 MB/s) | 2.2
42+
| 5 | 7ms (9.1 GB/s) | 29ms (2.2 GB/s) | 103ms (621 MB/s) | 92ms (696 MB/s) | 14.7
4343
| **Write**`[3]` | | |
44-
| 1 | 118ms (737 MB/s) | 165ms (527 MB/s) | 157ms (554 MB/s) | 718ms (121 MB/s) | 1.3
45-
| 2 | 71ms (802 MB/s) | 101ms (564 MB/s) | 137ms (416 MB/s) | 308ms (188 MB/s) | 1.9
46-
| 3 | 23ms (435 MB/s) | 29ms (344 MB/s) | 29ms (344 MB/s) | 101ms (99 MB/s) | 1.3
47-
| 4 | 16ms (625 MB/s) | 23ms (434 MB/s) | 42ms (238 MB/s) | 97ms (103 MB/s) | 2.6
48-
| 5 | 6.2ms (10 GB/s) | 46ms (1.4 GB/s) | 16ms (4.0 GB/s) | 21ms (3.0 GB/s) | 2.5
44+
| 1 | 99ms (879 MB/s) | 155ms (561 MB/s) | 157ms (554 MB/s) | 718ms (121 MB/s) | 1.6
45+
| 2 | 58ms (983 MB/s) | 79ms (722 MB/s) | 137ms (416 MB/s) | 308ms (188 MB/s) | 2.4
46+
| 3 | 17ms (588 MB/s) | 21ms (476 MB/s) | 29ms (344 MB/s) | 101ms (99 MB/s) | 1.7
47+
| 4 | 14ms (714 MB/s) | 17ms (588 MB/s) | 42ms (238 MB/s) | 97ms (103 MB/s) | 3.0
48+
| 5 | 6.6ms (9.7 GB/s) | 45ms (1.4 GB/s) | 16ms (4.0 GB/s) | 21ms (3.0 GB/s) | 2.4
4949
| **Read + Write** | |
50-
| 1 | 291ms (299 MB/s) | 377ms (231 MB/s) | 501ms (174 MB/s) | 1285 ms (68 MB/s) | 1.7
51-
| 2 | 173ms (329 MB/s) | 219ms (260 MB/s) | 306ms (186 MB/s) | 686 ms (83 MB/s) | 1.8
52-
| 3 | 57ms (176 MB/s) | 73ms (138 MB/s) | 94ms (106 MB/s) | 248ms (40 MB/s) | 1.6
53-
| 4 | 41ms (244 MB/s) | 51ms (196 MB/s) | 89ms (112 MB/s) | 252ms (40 MB/s) | 2.2
54-
| 5 | 16ms (4.0 GB/s) | 90ms (711 MB/s) | 119ms (537 MB/s) | 113ms (566 MB/s) | 7.4
50+
| 1 | 243ms (358 MB/s) | 304ms (286 MB/s) | 501ms (174 MB/s) | 1285 ms (68 MB/s) | 2.1
51+
| 2 | 137ms (416 MB/s) | 169ms (337 MB/s) | 306ms (186 MB/s) | 686 ms (83 MB/s) | 2.2
52+
| 3 | 47ms (213 MB/s) | 56ms (179 MB/s) | 94ms (106 MB/s) | 248ms (40 MB/s) | 2.0
53+
| 4 | 35ms (286 MB/s) | 38ms (263 MB/s) | 89ms (112 MB/s) | 252ms (40 MB/s) | 2.5
54+
| 5 | 14ms (4.7 GB/s) | 75ms (859 MB/s) | 119ms (537 MB/s) | 113ms (566 MB/s) | 8.5
5555

5656
<!-- | 3 | ms ( MB/s) | ms ( MB/s) | ms ( MB/s) | ms ( MB/s) | 0 -->
5757

@@ -74,22 +74,22 @@ We also compared QuickBuffers against the Java bindings of Google's [FlatBuffers
7474
| | QuickBuffers | FlatBuffers (v1.11.0) | FlatBuffers (v1.10.0) | Ratio`[1]`
7575
| :----------- | :-----------: | :-----------: | :-----------: | :-----------: |
7676
| **UnsafeSource / DirectByteBuffer [ns/op]**
77-
| Decode | 292 | 0 | 0 | 0.0
78-
| Traverse | 17 | 234 | 321 | 13.8
79-
| Encode | 312 | 457 | 649 | 1.5
80-
| Encode + Decode + Traverse | 621 | 691 | 970 | 1.1
77+
| Decode | 177 | 0 | 0 | 0.0
78+
| Traverse | 125 | 234 | 321 | 1.9
79+
| Encode | 233 | 457 | 649 | 2.0
80+
| Encode + Decode + Traverse | 523 | 691 | 970 | 1.3
8181
| **ArraySource / HeapByteBuffer [ns/op]**
82-
| Decode | 379 | 0 | 0 | 0.0
83-
| Traverse | 29 | 381 | 427 | 13.1
84-
| Encode | 334 | 626 | 821 | 1.9
85-
| Encode + Decode + Traverse | 742 | 1007 | 1248 | 1.4
82+
| Decode | 213 | 0 | 0 | 0.0
83+
| Traverse | 133 | 381 | 427 | 2.9
84+
| Encode | 268 | 626 | 821 | 2.3
85+
| Encode + Decode + Traverse | 614 | 1007 | 1248 | 1.6
8686
| **Other**
8787
| Serialized Size | 228 bytes | 344 bytes | 344 bytes | 1.5
8888
| Transient memory allocated during decode | 0 bytes | 0 bytes | 0 bytes | 1
8989

9090
* `[1]` `FlatBuffers v1.11.0 / QuickBuffers`
91-
* `[2]` `Traverse = (Decode + Traverse) - Decode`
91+
* `[2]` `Traverse = (Decode + Traverse) - Decode` (includes lazy utf8 parsing)
9292

9393
While the official C++ benchmark shows tremendous performance benefits over Protobuf, the Java implementation has unfortunately been lagging behind a bit. Recent versions have seen some significant performance improvements, but encoding and traversing a `ByteBuffer` still results in more overhead than may be expected.
9494

95-
Also be aware that the benchmark was created with a bias for FlatBuffers. The original data is mostly comprised of large varint numbers (e.g. a 10 byte int64) and repeated messages with multiple levels of nesting, which is a particularly bad case for Protobuf. Messages with a flatter hierarchy and more fixed-size scalar types should fare much better.
95+
It is also worth noting that the benchmark was created with a bias for FlatBuffers. The original data is mostly comprised of large varint numbers (e.g. a 10 byte int64) and repeated messages with multiple levels of nesting, which is a particularly bad case for Protobuf. Messages with a flatter hierarchy and more fixed-size scalar types should fare much better.

benchmarks/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@
4848
<artifactId>flatbuffers-java</artifactId>
4949
<version>1.11.0</version>
5050
</dependency>
51+
<dependency>
52+
<groupId>com.fasterxml.jackson.core</groupId>
53+
<artifactId>jackson-core</artifactId>
54+
<version>2.10.1</version>
55+
</dependency>
56+
<dependency>
57+
<groupId>com.google.code.gson</groupId>
58+
<artifactId>gson</artifactId>
59+
<version>2.8.6</version>
60+
</dependency>
5161
</dependencies>
5262

5363
<build>

benchmarks/src/main/java/us/hebi/quickbuf/benchmarks/comparison/PackedDoublesBenchmark.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@
4242
*
4343
* === QuickBuffers (Unsafe) ===
4444
* Benchmark Mode Cnt Score Error Units
45-
* PackedDoublesBenchmark.readRobo avgt 10 9.791 ± 0.331 ms/op -- 6.5 GB/s
46-
* PackedDoublesBenchmark.readWriteRobo avgt 10 16.167 ± 0.726 ms/op --
45+
* PackedDoublesBenchmark.readQuick avgt 10 7.061 ± 0.167 ms/op
46+
* PackedDoublesBenchmark.readWriteQuick avgt 10 13.618 ± 0.337 ms/op
4747
*
4848
* === QuickBuffers (Safe) ===
49-
* PackedDoublesBenchmark.readRobo avgt 10 44.434 ± 0.928 ms/op -- 1.5 GB/s
50-
* PackedDoublesBenchmark.readWriteRobo avgt 10 89.855 ± 3.870 ms/op --
49+
* PackedDoublesBenchmark.readQuick avgt 10 29.202 ± 0.397 ms/op
50+
* PackedDoublesBenchmark.readWriteQuick avgt 10 74.527 ± 1.353 ms/op
5151
*
5252
* === Java (Some Unsafe) ===
5353
* PackedDoublesBenchmark.readProto avgt 10 103.989 ± 37.389 ms/op
@@ -85,13 +85,13 @@ public static void main(String[] args) throws RunnerException {
8585
final Packed message = Packed.newInstance();
8686

8787
@Benchmark
88-
public Object readRobo() throws IOException {
88+
public Object readQuick() throws IOException {
8989
source.wrap(input);
9090
return message.clear().mergeFrom(source);
9191
}
9292

9393
@Benchmark
94-
public int readWriteRobo() throws IOException {
94+
public int readWriteQuick() throws IOException {
9595
message.clear().mergeFrom(source.wrap(input)).writeTo(sink.wrap(output));
9696
return sink.position();
9797
}

benchmarks/src/main/java/us/hebi/quickbuf/benchmarks/comparison/SbeBenchmark.java

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
* it under the terms of the GNU General Public License as
99
* published by the Free Software Foundation, either version 3 of the
1010
* License, or (at your option) any later version.
11-
*
11+
*
1212
* This program is distributed in the hope that it will be useful,
1313
* but WITHOUT ANY WARRANTY; without even the implied warranty of
1414
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1515
* GNU General Public License for more details.
16-
*
16+
*
1717
* You should have received a copy of the GNU General Public
1818
* License along with this program. If not, see
1919
* <http://www.gnu.org/licenses/gpl-3.0.html>.
@@ -51,16 +51,16 @@
5151
*
5252
* === QuickBuffers (Unsafe) ===
5353
* Benchmark Mode Cnt Score Error Units
54-
* SbeBenchmark.carUnsafeRoboRead avgt 10 33.602 ± 0.568 ms/op
55-
* SbeBenchmark.carUnsafeRoboReadReadWrite avgt 10 56.860 ± 1.176 ms/op
56-
* SbeBenchmark.marketUnsafeRoboRead avgt 10 25.022 ± 1.249 ms/op
57-
* SbeBenchmark.marketUnsafeRoboReadWrite avgt 10 41.227 ± 0.612 ms/op
54+
* SbeBenchmark.carUnsafeQuickRead avgt 10 30.661 ± 2.892 ms/op
55+
* SbeBenchmark.carUnsafeQuickReadReadWrite avgt 10 47.498 ± 2.198 ms/op
56+
* SbeBenchmark.marketUnsafeQuickRead avgt 10 21.946 ± 0.997 ms/op
57+
* SbeBenchmark.marketUnsafeQuickReadWrite avgt 10 35.220 ± 1.000 ms/op
5858
*
5959
* === QuickBuffers (Safe) ===
60-
* SbeBenchmark.carRoboRead avgt 10 44.306 ± 1.814 ms/op -- 226 MB/s
61-
* SbeBenchmark.carRoboReadWrite avgt 10 72.673 ± 3.227 ms/op -- 138 MB/s
62-
* SbeBenchmark.marketRoboRead avgt 10 28.309 ± 0.242 ms/op -- 353 MB/s
63-
* SbeBenchmark.marketRoboReadWrite avgt 10 51.189 ± 0.979 ms/op -- 196 MB/s
60+
* SbeBenchmark.carQuickRead avgt 10 35.540 ± 2.008 ms/op
61+
* SbeBenchmark.carQuickReadWrite avgt 10 55.914 ± 3.173 ms/op
62+
* SbeBenchmark.marketQuickRead avgt 10 21.027 ± 0.871 ms/op
63+
* SbeBenchmark.marketQuickReadWrite avgt 10 38.426 ± 0.743 ms/op
6464
*
6565
* === Protobuf-Java
6666
* Benchmark Mode Cnt Score Error Units
@@ -129,44 +129,44 @@ private static byte[] multiplyToNumBytes(byte[] singleMessage, int maxNumBytes)
129129

130130
// ===================== UNSAFE OPTION DISABLED (e.g. Android) =====================
131131
@Benchmark
132-
public int marketRoboRead() throws IOException {
133-
return readRobo(source.wrap(marketDataMessages), marketMsg);
132+
public int marketQuickRead() throws IOException {
133+
return readQuick(source.wrap(marketDataMessages), marketMsg);
134134
}
135135

136136
@Benchmark
137-
public int marketRoboReadWrite() throws IOException {
138-
return readWriteRobo(source.wrap(marketDataMessages), marketMsg, sink.wrap(output));
137+
public int marketQuickReadWrite() throws IOException {
138+
return readWriteQuick(source.wrap(marketDataMessages), marketMsg, sink.wrap(output));
139139
}
140140

141141
@Benchmark
142-
public int carRoboRead() throws IOException {
143-
return readRobo(source.wrap(carDataMessages), carMsg);
142+
public int carQuickRead() throws IOException {
143+
return readQuick(source.wrap(carDataMessages), carMsg);
144144
}
145145

146146
@Benchmark
147-
public int carRoboReadWrite() throws IOException {
148-
return readWriteRobo(source.wrap(carDataMessages), carMsg, sink.wrap(output));
147+
public int carQuickReadWrite() throws IOException {
148+
return readWriteQuick(source.wrap(carDataMessages), carMsg, sink.wrap(output));
149149
}
150150

151151
// ===================== UNSAFE OPTION ENABLED =====================
152152
@Benchmark
153-
public int marketUnsafeRoboRead() throws IOException {
154-
return readRobo(unsafeSource.wrap(marketDataMessages), marketMsg);
153+
public int marketUnsafeQuickRead() throws IOException {
154+
return readQuick(unsafeSource.wrap(marketDataMessages), marketMsg);
155155
}
156156

157157
@Benchmark
158-
public int marketUnsafeRoboReadWrite() throws IOException {
159-
return readWriteRobo(unsafeSource.wrap(marketDataMessages), marketMsg, unsafeSink.wrap(output));
158+
public int marketUnsafeQuickReadWrite() throws IOException {
159+
return readWriteQuick(unsafeSource.wrap(marketDataMessages), marketMsg, unsafeSink.wrap(output));
160160
}
161161

162162
@Benchmark
163-
public int carUnsafeRoboRead() throws IOException {
164-
return readRobo(unsafeSource.wrap(carDataMessages), carMsg);
163+
public int carUnsafeQuickRead() throws IOException {
164+
return readQuick(unsafeSource.wrap(carDataMessages), carMsg);
165165
}
166166

167167
@Benchmark
168-
public int carUnsafeRoboReadReadWrite() throws IOException {
169-
return readWriteRobo(unsafeSource.wrap(carDataMessages), carMsg, unsafeSink.wrap(output));
168+
public int carUnsafeQuickReadReadWrite() throws IOException {
169+
return readWriteQuick(unsafeSource.wrap(carDataMessages), carMsg, unsafeSink.wrap(output));
170170
}
171171

172172
// ===================== STOCK PROTOBUF =====================
@@ -191,7 +191,7 @@ public int carProtoReadWrite() throws IOException {
191191
}
192192

193193
// ===================== UTIL METHODS =====================
194-
static int readRobo(final ProtoSource source, final ProtoMessage message) throws IOException {
194+
static int readQuick(final ProtoSource source, final ProtoMessage message) throws IOException {
195195
while (!source.isAtEnd()) {
196196
final int length = source.readRawVarint32();
197197
int limit = source.pushLimit(length);
@@ -201,17 +201,11 @@ static int readRobo(final ProtoSource source, final ProtoMessage message) throws
201201
return source.getPosition();
202202
}
203203

204-
static int readWriteRobo(final ProtoSource source, final ProtoMessage message, final ProtoSink sink) throws IOException {
204+
static int readWriteQuick(final ProtoSource source, final ProtoMessage message, final ProtoSink sink) throws IOException {
205205
while (!source.isAtEnd()) {
206-
// read delimited
207-
final int length = source.readRawVarint32();
208-
int limit = source.pushLimit(length);
209-
message.clearQuick().mergeFrom(source);
210-
source.popLimit(limit);
211-
212-
// write delimited
213-
sink.writeRawVarint32(message.getSerializedSize());
214-
message.writeTo(sink);
206+
// read/write delimited
207+
source.readMessage(message.clearQuick());
208+
sink.writeMessageNoTag(message);
215209
}
216210
return sink.position();
217211
}

0 commit comments

Comments
 (0)