Java messaging bench (with the Kotlin supplementary cells)¶
This page records how the server-driven model of the specification (§10) is implemented in Java and how to run it. Comparison targets, patterns, units and judgement rules are owned by the specification; this page holds only the Java-specific values. Kotlin measures only the two supplementary cells of §10.5 (§7).
1. Comparison targets¶
| Implementation | request | send/command | API used |
|---|---|---|---|
grpc-java |
unary Echo on the grpc-java future stub |
unary Command, Empty reply |
grpc-java |
zlink-java |
raw ROUTER request(peer) |
command-only raw ROUTER send(peer) |
published systems.zlink:zlink |
zlink-framework-java |
ZLinkRouteClient.requestToChannel(...).submit(BenchPayload.class) |
sendToChannel(...).submit() |
zlink-framework-core, protobuf codec, Spring Boot starter |
grpc-kotlin (supplementary) |
BenchServiceCoroutineStub.echo suspend call |
none | grpc-kotlin |
zlink-framework-kotlin (supplementary) |
requestToChannel(...).awaitReply |
none | zlink-framework-kotlin |
Every row uses the same BenchPayload protobuf DTO and the 29-byte header in front of the body
(spec §6). raw sends the envelope and the protobuf body as two parts and keeps the request and
command endpoints separate.
2. How to run¶
cd framework/bench/grpc/java
./run_local.sh # full Java matrix
./run_local_kotlin.sh # the two Kotlin supplementary cells
RUNS=1 PAYLOADS=1024 SCENARIO=request-window IMPLEMENTATION=zlink-framework-java ./run_local.sh
Measurements always go through the perf ticket queue. The Kotlin cells reuse Java's B, so they never run at the same time as a Java measurement (the runner checks both port bands).
| Input | Java default | Kotlin default |
|---|---|---|
RUNS |
3 |
3 |
RUN_DEALER |
0 (other values rejected) |
0 |
DURATION |
5 |
5 |
WARMUP_SECONDS |
20 |
20 |
PAYLOADS |
1024,4096 |
1024 only |
SCENARIO |
all, request, one of the four patterns |
all, request, request-window |
IMPLEMENTATION |
all or one of three |
all or one of two |
STAMP / OUTROOT |
run time / log/java/with_grpc_java_<stamp> |
run time / log/java/with_grpc_kotlin_<stamp> |
SKIP_BUILD |
0 (1 skips the Gradle build) |
same |
Fixed: request window 100, send concurrency 8, request and route-ready timeout 30 s, drain ceiling
30 s, settle quiet period 200 ms. The build is ./gradlew --no-daemon --max-workers=1 assemble
installDist.
3. Process layout¶
| Cell | source A | A ports (trigger/stats) | target B | B ports |
|---|---|---|---|---|
grpc-java |
client: unary future stub |
5240/5241 | grpc-server: echo/count |
gRPC 5242, stats 5243 |
zlink-java |
client: raw ROUTER |
5245/5246 | zlink-raw-server: separate request and command ROUTERs |
request 5247, command 5248, stats 5249 |
zlink-framework-java |
client: ZLinkRouteClient |
5252/5253 | zlink-framework-server: typed request/send handlers |
RouteMesh 5254, stats 5255 |
grpc-kotlin (supplementary) |
kotlin-client: coroutine stub |
5260/5261 | Java grpc-server reused |
5242/5243 |
zlink-framework-kotlin (supplementary) |
kotlin-client: awaitReply |
5272/5273 | Java zlink-framework-server reused |
5254/5255 |
The trigger, stats and phase rules are owned by shared/.../BenchHttpApplication.java (JDK
HttpServer) alone. The trigger client is the runner's curl. The cell order is spec §10.4
(runner_common.sh) with a fresh A/B process pair per cell. The framework source reserves its fixed
HTTP ports first and then starts the RouteMesh listener. The runner checks LISTEN sockets on
5240-5259 (and 5260-5279 for Kotlin) before the run and after each cell.
| Pattern | streams.count |
streams.inFlightPerStream |
Implementation |
|---|---|---|---|
request-serial |
1 | 1 | sequential CompletableFutures on one platform submit thread |
request-window |
1 | 100 | one submit thread sharing one window across 100 CompletableFutures (100 coroutines in Kotlin) |
request-backpressure |
1 | none | one submit thread submitting without a ceiling and yielding to completions |
send-saturation |
8 | 1 | eight submit threads, each sending again after its completion notice |
4. Values set differently per language¶
| Item | Value |
|---|---|
| warmup | 20 s (1 s in smoke), same driver and streams as active |
| gRPC source | Java future stub or Kotlin coroutine stub, plaintext channel defaults |
| gRPC target | ServerBuilder.forPort, grpc-netty-shaded defaults, plaintext loopback |
| JDK | Temurin 22.0.2 (measured 2026-09-09; 25 from 1.0) |
| grpc-java / grpc-kotlin | 1.72.0 / 1.4.1 |
| protobuf-java | 4.30.2 |
| Kotlin / coroutines | 2.2.21 / 1.9.0 |
| ZLink binding | published 0.17.6 |
| framework | repository composite build (the measured commit is recorded in the raw file) |
| source saturation metric | jvm_thread_cores; the ceiling is the submit thread count (1 for serial/window/backpressure, 8 for send) |
5. Where results go¶
framework/bench/grpc/log/java/with_grpc_java_<stamp>/ # Kotlin: with_grpc_kotlin_<stamp>
├── with_grpc_java_<stamp>.txt
└── <implementation>-<pattern>-<payload>-run<n>/
├── results.json # with-grpc-cell-v1: role, trigger, streams, target_stats
├── report.txt
├── source.log / target.log
└── target-stats.json
python3 framework/bench/grpc/tools/bench_aggregate.py --lang java --judgement-pattern request-window \
--runs-glob 'framework/bench/grpc/log/java/<run-1>/*' \
--runs-glob 'framework/bench/grpc/log/java/<run-2>/*' \
--runs-glob 'framework/bench/grpc/log/java/<run-3>/*' \
--runs-glob 'framework/bench/grpc/log/c/<c-run>*' --format full
6. Known limits¶
zlink-java request-window: binding 0.17.6 loses every reply completion after a caller-ownedReceived.close()(decision record FB-050, reproduced byrepro/); that row is not published until the defect is fixed.- The raw comparison allows ROUTER↔ROUTER only.
request-backpressurehas no application in-flight ceiling, so the reached depth is the result.
7. Kotlin supplementary cells¶
Kotlin shares the binding, server and codec with Java, so it is excluded from the full matrix.
Only the two request-window @1024 cells (grpc-kotlin, zlink-framework-kotlin) run A in
Kotlin; B is the Java binary on the Java band. They appear next to the Java rows as supplementary
rows and are read as the cost of the Kotlin call layer.