package com.ankurm.protocols; import java.util.Iterator; import com.ankurm.protocols.grpc.StreamRequest; import com.ankurm.protocols.grpc.QuoteServiceGrpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.messaging.rsocket.RSocketStrategies; import static org.assertj.core.api.Assertions.assertThat; /** * Server streaming: how fast can one connection carry N messages one way? * *

This is the shape most of these protocols are actually chosen for — a price feed, a * log tail, a progress stream — and it separates them differently from request/response, * because the per-message cost stops being dominated by a round trip. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"spring.rsocket.server.port=7002", "spring.grpc.server.port=9092"}) class StreamThroughputBenchmarkTest { private static final int WARMUP = 5_000; private static final int N = 50_000; @LocalServerPort int httpPort; @Autowired RSocketStrategies strategies; @Test void fiftyThousandMessagesEachWay() throws Exception { System.out.println("=== server streaming, " + N + " messages on one connection, loopback ==="); System.out.println(grpc()); System.out.println(rsocket()); System.out.println(websocket()); } private String grpc() { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9092) .usePlaintext().build(); try { var stub = QuoteServiceGrpc.newBlockingStub(channel); drain(stub, WARMUP); long t0 = System.nanoTime(); int seen = drain(stub, N); return report("gRPC", seen, System.nanoTime() - t0); } finally { channel.shutdownNow(); } } private int drain(QuoteServiceGrpc.QuoteServiceBlockingStub stub, int count) { // The blocking stub returns an Iterator. Errors surface mid-iteration rather than at the // call, which is the first thing that surprises people about gRPC server streaming. Iterator it = stub.streamQuotes( StreamRequest.newBuilder().setSymbol("AAPL").setCount(count).build()); int seen = 0; while (it.hasNext()) { it.next(); seen++; } return seen; } private String rsocket() { RSocketRequester requester = RSocketRequester.builder() .rsocketStrategies(strategies).tcp("localhost", 7002); try { var warm = new RSocketQuoteController.StreamSpec("AAPL", WARMUP, 0); requester.route("quotes").data(warm).retrieveFlux(Quote.class).blockLast(); var spec = new RSocketQuoteController.StreamSpec("AAPL", N, 0); long t0 = System.nanoTime(); long seen = requester.route("quotes").data(spec) .retrieveFlux(Quote.class).count().block(); return report("RSocket", (int) seen, System.nanoTime() - t0); } finally { requester.dispose(); } } private String websocket() throws Exception { // A generous inbox, because there is nothing else to do with messages that arrive faster // than they are read. This IS the WebSocket back-pressure story, in one constant. try (WsClient client = new WsClient(httpPort, N + WARMUP + 16)) { client.send("STREAM AAPL " + WARMUP + " 0"); int warm = 0; String m; while ((m = client.take(20_000)) != null && !"END".equals(m)) { warm++; } assertThat(warm).isEqualTo(WARMUP); long t0 = System.nanoTime(); client.send("STREAM AAPL " + N + " 0"); int seen = 0; while ((m = client.take(20_000)) != null && !"END".equals(m)) { seen++; } return report("WebSocket", seen, System.nanoTime() - t0); } } private String report(String label, int seen, long elapsedNanos) { assertThat(seen).isEqualTo(N); double seconds = elapsedNanos / 1e9; return String.format("%-12s %,7d msgs in %6.3f s = %,10.0f msgs/s (%6.2f us each)", label, seen, seconds, seen / seconds, elapsedNanos / 1000.0 / seen); } }