Add the protocol-comparison module
This commit is contained in:
83
protocol-comparison/docs/01-three-protocols.md
Normal file
83
protocol-comparison/docs/01-three-protocols.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 1. The same use case, three ways
|
||||
|
||||
Next: [2. How they measure](02-benchmarks.md)
|
||||
|
||||
---
|
||||
|
||||
One application in this module serves the same two operations over gRPC, RSocket and a raw
|
||||
WebSocket: fetch one quote, and stream N quotes. Same JVM, same heap, same
|
||||
[`QuoteSource`](../src/main/java/com/ankurm/protocols/QuoteSource.java), same five fields.
|
||||
|
||||
That constraint is the point. Cross-protocol benchmarks published on the internet almost always
|
||||
compare three different applications, and end up measuring three different serialisation
|
||||
libraries, three JIT states and three thread pools.
|
||||
|
||||
## What each one is, in one sentence
|
||||
|
||||
| | gRPC | RSocket | Raw WebSocket |
|
||||
|---|---|---|---|
|
||||
| Transport | HTTP/2 | TCP, WebSocket, or others | HTTP/1.1 Upgrade |
|
||||
| Schema | `.proto`, mandatory, code-generated | none required | none |
|
||||
| Encoding here | protobuf | CBOR (Spring's default) | JSON |
|
||||
| Interaction models | 4, declared in the `.proto` | 4, chosen by return type | whatever you invent |
|
||||
| Demand signalling | none in the API | `request(n)` on the wire | none at all |
|
||||
| Deadlines | per call, absolute, propagating | none built in | none |
|
||||
| Browser client | needs grpc-web + a proxy | yes, over the WebSocket transport | yes, natively |
|
||||
| Boot 4 starters | `spring-boot-starter-grpc-server` / `-client` | `spring-boot-starter-rsocket` | `spring-boot-starter-websocket` |
|
||||
|
||||
## The three server sides, side by side
|
||||
|
||||
**gRPC** ([`GrpcQuoteService`](../src/main/java/com/ankurm/protocols/GrpcQuoteService.java)) is
|
||||
generated-class inheritance. There is no registration code: the generated `ImplBase` is a
|
||||
`BindableService` and Boot registers every such bean.
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void getQuote(QuoteRequest request, StreamObserver<Quote> observer) {
|
||||
observer.onNext(toProto(source.at(request.getSymbol(), 0)));
|
||||
observer.onCompleted();
|
||||
}
|
||||
```
|
||||
|
||||
**RSocket** ([`RSocketQuoteController`](../src/main/java/com/ankurm/protocols/RSocketQuoteController.java))
|
||||
is annotation routing, and the interaction model *is the return type*:
|
||||
|
||||
```java
|
||||
@MessageMapping("quote")
|
||||
public Mono<Quote> quote(String symbol) { ... } // request/response
|
||||
|
||||
@MessageMapping("quotes")
|
||||
public Flux<Quote> quotes(StreamSpec spec) { ... } // request/stream
|
||||
```
|
||||
|
||||
**Raw WebSocket** ([`WebSocketQuoteHandler`](../src/main/java/com/ankurm/protocols/WebSocketQuoteHandler.java))
|
||||
is a `switch` on a string, because there is nothing else:
|
||||
|
||||
```java
|
||||
switch (parts[0]) {
|
||||
case "QUOTE" -> send(session, source.at(parts[1], 0));
|
||||
case "STREAM" -> { for (int i = 0; i < count; i++) { send(...); } }
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
That third block is the honest summary of what a raw WebSocket gives you: a bidirectional pipe
|
||||
for text or bytes. Everything above it — routing, correlation, errors, versioning, demand
|
||||
— is yours to invent, and inventing it is how teams end up with a private, undocumented
|
||||
protocol that only its authors can debug. STOMP exists to stop that, and is covered in
|
||||
[`../sse-websocket`](../../sse-websocket/README.md).
|
||||
|
||||
## Boot 4 version notes
|
||||
|
||||
Boot 4.1.1's BOM pins **grpc-java 1.83.1** and **protobuf-java 4.35.1**, both deliberately older
|
||||
than the newest releases on Maven Central. Overriding them independently is how you get a
|
||||
`NoSuchMethodError` between grpc-java and its shaded Netty. Note that these have moved since Boot
|
||||
4.1.0, which pinned 1.80.0 and 4.34.2 — so a `spring.grpc.*` guide written against 4.1.0 is
|
||||
already describing different jars.
|
||||
|
||||
RSocket is **rsocket-java 1.1.5**, and `spring-boot-starter-rsocket` brings
|
||||
`jackson-dataformat-cbor` on purpose: CBOR, not JSON, is Spring's default RSocket data mime type.
|
||||
|
||||
---
|
||||
|
||||
Next: [2. How they measure](02-benchmarks.md)
|
||||
74
protocol-comparison/docs/02-benchmarks.md
Normal file
74
protocol-comparison/docs/02-benchmarks.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 2. How they measure
|
||||
|
||||
Previous: [1. Three protocols](01-three-protocols.md) · Next: [3. Payload](03-payload.md)
|
||||
|
||||
---
|
||||
|
||||
## Read this before the numbers
|
||||
|
||||
Client and server are **the same process on one machine**, talking over loopback: two cores,
|
||||
3.9 GB, JDK 25, Boot 4.1.1. That has three consequences, and ignoring them makes the numbers
|
||||
say the opposite of the truth.
|
||||
|
||||
1. **There is no network.** On a real link, a 35-byte message and an 83-byte message differ by
|
||||
more than the framing cost, and protobuf's size advantage starts paying. Here it does not.
|
||||
2. **A protocol that does less looks faster.** WebSocket wins both benchmarks below partly because
|
||||
it has no flow control, no deadlines, no schema and no per-call metadata. That is a real cost
|
||||
difference, and it is also exactly what you give up.
|
||||
3. **Ratios, not absolutes.** Absolute microsecond figures from a two-core sandbox mean nothing
|
||||
for your hardware. The ordering and the rough factors are what transfer.
|
||||
|
||||
Re-measured on every `./scripts/run-all.sh`, and re-measured **in their own JVM invocation**.
|
||||
That is not tidiness. The back-pressure tests in [chapter 4](04-backpressure.md) leave unbounded
|
||||
producers spinning, and running the throughput benchmark after them on a two-core box halved
|
||||
every number — gRPC fell from 44 801 msgs/s to 18 477. If you take one operational lesson from
|
||||
this module rather than a protocol one, let it be that: a benchmark that shares a machine with
|
||||
anything is measuring the machine.
|
||||
|
||||
## Request/response
|
||||
|
||||
5 000 sequential calls after 2 000 warm-up
|
||||
([`request-response.txt`](output/request-response.txt)):
|
||||
|
||||
```
|
||||
gRPC n=5000 p50= 291.3 us p99= 1015.3 us mean= 324.4 us ~3,083 calls/s
|
||||
RSocket n=5000 p50= 300.9 us p99= 1337.4 us mean= 350.9 us ~2,850 calls/s
|
||||
WebSocket n=5000 p50= 124.3 us p99= 1447.8 us mean= 172.8 us ~5,786 calls/s
|
||||
```
|
||||
|
||||
**gRPC and RSocket are the same speed.** Within noise across runs the two swap places; treat them
|
||||
as indistinguishable for request/response on a warm connection.
|
||||
|
||||
**Raw WebSocket is about twice as fast at the median — and has the worst tail.** p50 of
|
||||
124 µs against a p99 of 1 448 µs is a 12x spread. gRPC's is 3.5x. The
|
||||
median is cheap because a text frame over an already-open socket is nearly free; the tail is bad
|
||||
because nothing is scheduling anything, so a GC pause or a scheduling hiccup lands on the request
|
||||
undiluted.
|
||||
|
||||
If your SLO is a percentile rather than an average — and it should be — that table
|
||||
does not say what a first glance suggests.
|
||||
|
||||
## Server streaming
|
||||
|
||||
50 000 messages on one connection
|
||||
([`stream-throughput.txt`](output/stream-throughput.txt)):
|
||||
|
||||
```
|
||||
gRPC 50,000 msgs in 1.116 s = 44,801 msgs/s ( 22.32 us each)
|
||||
RSocket 50,000 msgs in 0.702 s = 71,175 msgs/s ( 14.05 us each)
|
||||
WebSocket 50,000 msgs in 0.459 s = 109,022 msgs/s ( 9.17 us each)
|
||||
```
|
||||
|
||||
Same ordering, wider gaps: WebSocket about 2.4x gRPC, RSocket about 1.6x.
|
||||
|
||||
Now read the WebSocket row again with its harness in view. The client's inbox is an
|
||||
`ArrayBlockingQueue` sized to hold **all fifty thousand messages**, because there was nothing else
|
||||
to do with them — a raw WebSocket consumer that falls behind can buffer or drop, and those
|
||||
are the only two options. WebSocket "won" this benchmark by buffering the entire stream in the
|
||||
client's heap.
|
||||
|
||||
That is not a rhetorical point. It is the next chapter.
|
||||
|
||||
---
|
||||
|
||||
Previous: [1. Three protocols](01-three-protocols.md) · Next: [3. Payload](03-payload.md)
|
||||
56
protocol-comparison/docs/03-payload.md
Normal file
56
protocol-comparison/docs/03-payload.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 3. Payload: the one comparison that needs no benchmark
|
||||
|
||||
Previous: [2. Benchmarks](02-benchmarks.md) · Next: [4. Back-pressure](04-backpressure.md)
|
||||
|
||||
---
|
||||
|
||||
Five fields — a symbol, a sequence number, two doubles and a timestamp — encoded three
|
||||
ways ([`payload-sizes.txt`](output/payload-sizes.txt)):
|
||||
|
||||
```
|
||||
protobuf : 35 bytes 0a044141504c102a197b14ae47e11a5940215c8fc2f5281c5940288080abb4fef39203
|
||||
JSON : 83 bytes {"symbol":"AAPL","seq":42,"bid":100.42,"ask":100.44,"epochMicros":1772000000000000}
|
||||
CBOR : 67 bytes bf6673796d626f6c644141504c63736571182a63626964fb40591ae147ae147b6361736bfb40591c28f5c28f5c6b65706f63684d6963726f731b00064b9fe68ac000ff
|
||||
JSON is 2.37x protobuf; CBOR is 1.91x protobuf
|
||||
```
|
||||
|
||||
This is a property of the formats, not of a machine, so it is the one row of the comparison that
|
||||
transfers to your hardware unchanged.
|
||||
|
||||
**Protobuf is 35 bytes because field names are integers.** `0a04 41 41 50 4c` is field 1, length
|
||||
4, `AAPL`. There is no `"symbol"` on the wire at all — the name lives in the `.proto` file
|
||||
that both sides compiled against. That is where the size comes from, and it is also the whole
|
||||
argument about schemas: the saving and the coupling are the same fact.
|
||||
|
||||
**CBOR is 67 bytes and still writes the field names.** `6673796d626f6c` is a 6-character text
|
||||
string, `symbol`. CBOR is binary JSON, not a schema format: it saves the punctuation and encodes
|
||||
numbers compactly, and it keeps every key. So its advantage over JSON is real but modest, and it
|
||||
does not require the coupling.
|
||||
|
||||
Concretely, on a stream of a million quotes: 35 MB, 67 MB, 83 MB. On a loopback
|
||||
that difference disappears into memory bandwidth — which is exactly why the benchmarks in
|
||||
[chapter 2](02-benchmarks.md) do not reward it and a real link would.
|
||||
|
||||
Two practical notes:
|
||||
|
||||
- **Spring's RSocket default is CBOR**, not JSON, which is why
|
||||
`spring-boot-starter-rsocket` brings `jackson-dataformat-cbor`. If you are debugging with
|
||||
`tcpdump` and expecting to read your payloads, that is why you cannot. The mechanism is codec
|
||||
ordering, printed by
|
||||
[`RSocketDefaultsTest`](../src/test/java/com/ankurm/protocols/RSocketDefaultsTest.java):
|
||||
|
||||
```
|
||||
encoders : [CharSequenceEncoder, ByteBufferEncoder, ByteArrayEncoder, DataBufferEncoder, JacksonCborEncoder, JacksonJsonEncoder]
|
||||
bare RSocketStrategies.create() : [CharSequenceEncoder, ByteBufferEncoder, ByteArrayEncoder, DataBufferEncoder]
|
||||
```
|
||||
|
||||
CBOR sits *ahead of* JSON in the Boot-configured list, which is the whole of why it wins. Note
|
||||
also that a bare `RSocketStrategies.create()` carries neither — an `RSocketRequester` built
|
||||
without injecting Boot's strategies cannot encode your objects at all. And the class names
|
||||
follow the Boot 4 rule: `JacksonCborEncoder`, not `Jackson2…`.
|
||||
- **You can put protobuf on RSocket.** The encoding and the protocol are independent choices;
|
||||
the row above is what each stack does *by default*, not what it is capable of.
|
||||
|
||||
---
|
||||
|
||||
Previous: [2. Benchmarks](02-benchmarks.md) · Next: [4. Back-pressure](04-backpressure.md)
|
||||
112
protocol-comparison/docs/04-backpressure.md
Normal file
112
protocol-comparison/docs/04-backpressure.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 4. Back-pressure: the measurement that decides it
|
||||
|
||||
Previous: [3. Payload](03-payload.md) · Next: [5. Choosing](05-choosing.md)
|
||||
|
||||
---
|
||||
|
||||
Every protocol comparison eventually reduces to one question, and it is not throughput. It is:
|
||||
|
||||
> **When the consumer stops keeping up, what happens?**
|
||||
|
||||
[`BackPressureTest`](../src/test/java/com/ankurm/protocols/BackPressureTest.java) asks it the
|
||||
simplest way there is. Each server offers an unbounded stream. Each client takes a hundred
|
||||
messages and then does nothing. How many did the server produce?
|
||||
|
||||
The sweep over quiet periods is what makes the answer conclusive. A number that stays flat as the
|
||||
wait grows means something bounded the producer. A number that keeps climbing means nothing did.
|
||||
|
||||
[`backpressure.txt`](output/backpressure.txt):
|
||||
|
||||
```
|
||||
protocol quiet 0.5s quiet 1s quiet 2s
|
||||
RSocket 100 100 100
|
||||
gRPC 103,425 449,318 1,208,219
|
||||
WebSocket 19,664 143,870 320,255
|
||||
```
|
||||
|
||||
RSocket produced **exactly one hundred**, at every quiet period. gRPC produced over a million in
|
||||
two seconds. Raw WebSocket, three hundred thousand. These are re-measured on every run and the
|
||||
absolute counts move by tens of percent; the three orders of magnitude between the rows do not.
|
||||
|
||||
(The growth is faster than linear because the loop is still being JIT-compiled during the shortest
|
||||
run. The shape is the finding, not the exponent.)
|
||||
|
||||
## Why RSocket stops
|
||||
|
||||
`request(n)` is a **frame on the wire**. When the client's subscriber calls `subscription.request(100)`,
|
||||
a REQUEST_N frame carries that number to the server, and Reactor's `Flux.generate` is called
|
||||
exactly a hundred times and then not again. Nothing blocks; there is no buffer to size and no
|
||||
thread parked. The producer simply is not invoked.
|
||||
|
||||
That is what "Reactive Streams over the network" means, and it is the entire reason RSocket
|
||||
exists. The same property is why RSocket's streaming throughput in
|
||||
[chapter 2](02-benchmarks.md) is respectable rather than the highest: it is doing real work per
|
||||
batch that the others are not doing at all.
|
||||
|
||||
## Why the other two do not
|
||||
|
||||
**Raw WebSocket has no mechanism.** The frame protocol has no notion of demand. The server writes
|
||||
until something underneath refuses — a socket buffer, Spring's `sendBufferSizeLimit`, the
|
||||
heap. Every one of those 320 255 messages was serialised and written for a client that had
|
||||
asked for a hundred.
|
||||
|
||||
**gRPC has HTTP/2 flow control, and it did not help.** This is the result I did not expect.
|
||||
`StreamObserver.onNext` on a gRPC server **does not block**, so a plain server-streaming loop keeps
|
||||
producing regardless of what the client is doing.
|
||||
|
||||
The documented answer is `ServerCallStreamObserver.isReady()`, and this module tests it rather than
|
||||
citing it ([`grpc-isready.txt`](output/grpc-isready.txt)):
|
||||
|
||||
```
|
||||
handler quiet 0.5s quiet 2s
|
||||
plain onNext loop 258,145 1,421,778
|
||||
isReady() checked before write 667,259 1,044,018
|
||||
```
|
||||
|
||||
**It did not bound the producer.** The `isReady()` row is not lower than the plain one in any
|
||||
useful sense — it produced 667 thousand messages in half a second for a client that
|
||||
wanted a hundred — and across runs it has come out both far above and far below the plain
|
||||
loop, which is its own kind of answer. I
|
||||
have not established whether the surplus accumulates in the server's outbound queue, in the client
|
||||
transport, or is discarded after the deframer, and I am not going to assert a mechanism I have not
|
||||
read the source for.
|
||||
|
||||
What the numbers do establish is narrower and still worth knowing: **`isReady()` is not a demand
|
||||
signal**, and on a fast link with a stalled consumer it does not behave like one. If you are
|
||||
streaming from gRPC to a consumer that can fall behind, the bound has to come from your own
|
||||
design — a paged API, an explicit "send me more" message on a bidirectional stream, or a
|
||||
`count` in the request as `StreamQuotes` uses.
|
||||
|
||||
## The honest caveat
|
||||
|
||||
A loopback connection is the worst case for this test in the fairest possible way: the wire drains
|
||||
instantly, so nothing downstream ever pushes back. On a slow real link, TCP and the HTTP/2 window
|
||||
*would* eventually stop the gRPC and WebSocket producers — by blocking a thread or by
|
||||
filling a buffer, but they would stop.
|
||||
|
||||
That does not rescue them, because the failure mode you actually meet is not "the network is
|
||||
slow", it is "one consumer of forty is slow", and a shared server discovers that as latency for
|
||||
everyone else or as a heap that will not shrink. The difference RSocket makes is that the slow
|
||||
consumer's slowness is *expressed*, in a number, that the producer can act on before anything
|
||||
fills up.
|
||||
|
||||
## The other thing gRPC does not do for you
|
||||
|
||||
Abandoning a gRPC stream produced this, repeatedly, in the server log:
|
||||
|
||||
```
|
||||
java.lang.IllegalStateException: Failed to close the call
|
||||
Caused by: java.lang.IllegalStateException: call already closed
|
||||
```
|
||||
|
||||
That is the cancellation story from
|
||||
[the Spring gRPC article](https://ankurm.com/spring-grpc-spring-boot-4/) showing up in practice:
|
||||
gRPC does not interrupt your thread when a client leaves, it sets a flag on the `Context`, and a
|
||||
handler that touches an already-closed stream throws. The loop in
|
||||
[`GrpcQuoteService`](../src/main/java/com/ankurm/protocols/GrpcQuoteService.java) checks
|
||||
`Context.current().isCancelled()` and returns without calling `onCompleted` for exactly this
|
||||
reason.
|
||||
|
||||
---
|
||||
|
||||
Previous: [3. Payload](03-payload.md) · Next: [5. Choosing](05-choosing.md)
|
||||
63
protocol-comparison/docs/05-choosing.md
Normal file
63
protocol-comparison/docs/05-choosing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 5. Choosing
|
||||
|
||||
Previous: [4. Back-pressure](04-backpressure.md)
|
||||
|
||||
---
|
||||
|
||||
## The decision table
|
||||
|
||||
| If | Pick | Because |
|
||||
|---|---|---|
|
||||
| Service-to-service RPC, polyglot team, you want a schema | **gRPC** | The `.proto` is a contract other languages generate from. Deadlines are absolute and propagate. Boot 4 makes it a first-class starter. |
|
||||
| A stream whose consumer can fall behind | **RSocket** | `request(n)` is the only demand signal that crosses the wire. 100 vs 1.3 million is not a tuning difference. |
|
||||
| A browser is one of the peers | **WebSocket**, with STOMP on it | gRPC needs grpc-web and a proxy. RSocket over WebSocket works but the client ecosystem is thin. |
|
||||
| Server pushes, client never speaks | **none of these** — use SSE | See [`../sse-websocket`](../../sse-websocket/README.md). |
|
||||
| You want the lowest possible median latency and control both ends | **raw WebSocket** | It is fastest here because it does the least. Read the p99 before deciding that is what you want. |
|
||||
| Long-lived bidirectional exchange with independent streams | **RSocket channel** or **gRPC bidi** | Both give you two independent streams; only RSocket gives each of them demand. |
|
||||
|
||||
## What each one really costs
|
||||
|
||||
**gRPC** costs a build step, a schema you must version, and an operational story for HTTP/2 through
|
||||
your proxies. In exchange you get the best-supported cross-language RPC there is, and deadlines
|
||||
— which is a bigger deal than it sounds, because an absolute deadline that propagates across
|
||||
hops is the single most effective defence against one slow dependency taking down a system.
|
||||
|
||||
**RSocket** costs ecosystem. It is the least deployed of the three, the client libraries outside
|
||||
Java and JavaScript are thin, and if you are not already reactive, `Mono` and `Flux` in your
|
||||
service signatures is a real commitment. In exchange you get the only demand signalling in the
|
||||
list, plus resumption and leasing.
|
||||
|
||||
**Raw WebSocket** costs everything you then invent: routing, correlation, error shape, versioning,
|
||||
and the demand mechanism you will eventually need. That is the cost STOMP exists to prevent, and
|
||||
if you find yourself designing a text protocol with a command word at the front — as
|
||||
[`WebSocketQuoteHandler`](../src/main/java/com/ankurm/protocols/WebSocketQuoteHandler.java)
|
||||
deliberately does — that is the signal to stop and use one.
|
||||
|
||||
## Should you use any of them?
|
||||
|
||||
Three cases where the answer is no:
|
||||
|
||||
- **Request/response between two services that both speak HTTP already.** A REST endpoint with a
|
||||
`RestClient` and a timeout is fine, and everything in your organisation already knows how to
|
||||
observe, cache, proxy and debug it. Reach for gRPC when the schema or the deadline propagation
|
||||
is the thing you need, not because it benchmarks faster.
|
||||
- **Streaming that is really batching.** If the consumer processes a page at a time anyway,
|
||||
paginated HTTP has back-pressure for free: the consumer asks for the next page when it is ready.
|
||||
That is `request(n)` with a URL.
|
||||
- **Events that outlive the connection.** None of these three persist anything. A dropped
|
||||
connection loses the messages in flight. If that matters, the answer is a broker —
|
||||
[`../kafka-basics`](../../kafka-basics/README.md),
|
||||
[`../rabbitmq`](../../rabbitmq/README.md), and the comparison in
|
||||
[`../broker-comparison`](../../broker-comparison/README.md).
|
||||
|
||||
## And the benchmark caveat, one more time
|
||||
|
||||
Every number in this module was measured with client and server in one JVM on one machine. That
|
||||
deletes the network, which is where protobuf's 35 bytes would pay and where the tail latencies
|
||||
would look completely different. The measurement that does transfer is
|
||||
[chapter 4](04-backpressure.md), because "how many did the server produce for a consumer that
|
||||
wanted a hundred" is a property of the protocol and not of the link.
|
||||
|
||||
---
|
||||
|
||||
Previous: [4. Back-pressure](04-backpressure.md)
|
||||
10
protocol-comparison/docs/output/backpressure.txt
Normal file
10
protocol-comparison/docs/output/backpressure.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
=== unbounded stream; client takes 100, then goes quiet ===
|
||||
protocol quiet 0.5s quiet 1s quiet 2s
|
||||
RSocket 100 100 100
|
||||
gRPC 103,425 449,318 1,208,219
|
||||
WebSocket 19,664 143,870 320,255
|
||||
RSocket is flat: the server produced exactly what request(n) asked for.
|
||||
gRPC grows 11.7x between 0.5 s and 2 s; WebSocket grows 16.3x.
|
||||
Growth that tracks the wait, rather than settling at a buffer size,
|
||||
means nothing bounded the producer. (Growth is faster than linear
|
||||
because the loop is still being JIT-compiled during the first run.)
|
||||
4
protocol-comparison/docs/output/grpc-isready.txt
Normal file
4
protocol-comparison/docs/output/grpc-isready.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
=== gRPC server streaming, client takes 100 then stops ===
|
||||
handler quiet 0.5s quiet 2s
|
||||
plain onNext loop 258,145 1,421,778
|
||||
isReady() checked before write 667,259 1,044,018
|
||||
5
protocol-comparison/docs/output/payload-sizes.txt
Normal file
5
protocol-comparison/docs/output/payload-sizes.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
=== one Quote, five fields, three encodings ===
|
||||
protobuf : 35 bytes 0a044141504c102a197b14ae47e11a5940215c8fc2f5281c5940288080abb4fef39203
|
||||
JSON : 83 bytes {"symbol":"AAPL","seq":42,"bid":100.42,"ask":100.44,"epochMicros":1772000000000000}
|
||||
CBOR : 67 bytes bf6673796d626f6c644141504c63736571182a63626964fb40591ae147ae147b6361736bfb40591c28f5c28f5c6b65706f63684d6963726f731b00064b9fe68ac000ff
|
||||
JSON is 2.37x protobuf; CBOR is 1.91x protobuf
|
||||
4
protocol-comparison/docs/output/request-response.txt
Normal file
4
protocol-comparison/docs/output/request-response.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
=== request/response, 5000 sequential calls after 2000 warm-up, loopback, JDK 25.0.4.1 ===
|
||||
gRPC n=5000 p50= 302.0 us p99= 994.7 us mean= 335.1 us ~2,984 calls/s
|
||||
RSocket n=5000 p50= 265.9 us p99= 1166.2 us mean= 311.9 us ~3,206 calls/s
|
||||
WebSocket n=5000 p50= 122.8 us p99= 1893.9 us mean= 184.8 us ~5,410 calls/s
|
||||
4
protocol-comparison/docs/output/rsocket-codecs.txt
Normal file
4
protocol-comparison/docs/output/rsocket-codecs.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
=== Boot-configured RSocketStrategies ===
|
||||
encoders : [CharSequenceEncoder, ByteBufferEncoder, ByteArrayEncoder, DataBufferEncoder, JacksonCborEncoder, JacksonJsonEncoder]
|
||||
decoders : [StringDecoder, ByteBufferDecoder, ByteArrayDecoder, DataBufferDecoder, JacksonCborDecoder, JacksonJsonDecoder]
|
||||
bare RSocketStrategies.create() encoders : [CharSequenceEncoder, ByteBufferEncoder, ByteArrayEncoder, DataBufferEncoder]
|
||||
4
protocol-comparison/docs/output/stream-throughput.txt
Normal file
4
protocol-comparison/docs/output/stream-throughput.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
=== server streaming, 50000 messages on one connection, loopback ===
|
||||
gRPC 50,000 msgs in 1.341 s = 37,285 msgs/s ( 26.82 us each)
|
||||
RSocket 50,000 msgs in 0.900 s = 55,580 msgs/s ( 17.99 us each)
|
||||
WebSocket 50,000 msgs in 0.839 s = 59,630 msgs/s ( 16.77 us each)
|
||||
6
protocol-comparison/docs/output/tests.txt
Normal file
6
protocol-comparison/docs/output/tests.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.321 s -- in com.ankurm.protocols.PayloadSizeTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.76 s -- in com.ankurm.protocols.RequestResponseBenchmarkTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.29 s -- in com.ankurm.protocols.BackPressureTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.409 s -- in com.ankurm.protocols.GrpcIsReadyTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.474 s -- in com.ankurm.protocols.RSocketDefaultsTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.244 s -- in com.ankurm.protocols.StreamThroughputBenchmarkTest
|
||||
Reference in New Issue
Block a user