Receive path goes value-only: KeyExpr string-backed except declared, Encoding always (id, schema)#489
Conversation
zenoh-flat-jni#6 makes the delivered encoding handle CONDITIONAL on schema presence (binding-local encoding_if_schema behind prebindgen#84's field! leaf): a received preset re-sends through the id arm for free, so no per-message native handle — clone + Box + JVM wrapper + gc-Cleaner registration + Cleaner free — is ever materialized for it. Schema- carrying encodings keep the send-ready handle. Adapt the delivery plumbing: Sample.fromParts encH becomes nullable (Query's already was), the reply callback drops its non-null assertion, and EncodingHandleTest's preset round-trip flips to assert the received copy has NO handle — the new contract. CI pin bumped to the flat-jni#6 tip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous pin was a branch commit deleted on merge; 1e040c9 is ZettaScaleLabs/zenoh-flat-jni#6 on main (conditional encoding handle, final fun!+sig! syntax). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consumes zenoh-flat-jni's string-delivering KeyExpr output. Ownership model (the Encoding model transplanted): a native handle exists ONLY behind Session.declareKeyExpr — the single case zenoh attaches a wire declaration (the only state beyond the string, paying off only on send through the declaring session). Everything else is a plain string value: - tryFrom validates natively once and releases the probe handle; autocanonize reads the canonized string and releases; - RECEIVED keyexprs (sample/query/reply) arrive as ONE eager string — no native allocation, nothing to free; the old per-message handle never carried a declaration (zenoh's RX path builds declaration-less) so it bought nothing on re-send while costing an unfreeable alloc and a second getStr crossing for every string-reading consumer; - sends pick the string or handle selector arm via KeyExpr.jniSel/jniStr/jniHandle (cloneHandle for consuming params); - algebra ops use the declared handle or a transient probe; - undeclare demotes to string-backed; close() is meaningful only for declared instances (doc matches upstream again). 113 jvmTest green incl. the new KeyExprHandleTest (constructed/declared/ received ownership + saved-keyexpr re-publish). CI pin bumped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Benchmark results (
|
| payload | baseline | string-backed KeyExpr | delta |
|---|---|---|---|
| 8 B | 982,596 msg/s | 985,754 msg/s | +0.3% (noise) |
| 1 KB | 733,136 msg/s | 744,738 msg/s | +1.6% |
Throughput is neutral-to-slightly-positive — the eager jstring costs about what the deferred getStr crossing did (the thr subscriber reads keyExpr.toString() per message, so the baseline paid that second crossing anyway) — and the per-message native KeyExpr leak is eliminated outright: no allocation to leak, no gc_managed tax (which had measured −23%).
Side observation: both of today's runs sit ~10% above the pre-#6 baseline measured earlier (891k @8B) — the conditional Encoding handle from ZettaScaleLabs/zenoh-flat-jni#6 removing the per-message preset-encoding lifecycle. Cross-day comparison, so indicative only, but consistent with the microbench prediction.
🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up: with the hot path now handle-free, flat-jni's KeyExpr became |
ZPing puts carry a custom schema-carrying encoding and verify (after warmup) that the echoed sample brings it back; ZPong retransmits the RECEIVED payload AND encoding — a pong that drops metadata is not a faithful echo, and the round trip now measures the real receive-then-resend path the send-ready Encoding handle was built for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Encoding handle A/B: ping-pong with real receive-then-resend (
|
| payload | A (handle) | B (no handle) |
|---|---|---|
| 8 B | 68 / 67 / 69 µs | 58 / 65 / 47 µs |
| 1 KB | 68 / 70 / 67 µs | 67 / 66 / 70 µs |
Verdict: the handle machinery buys nothing measurable in its own motivating scenario — and mechanically costs more than it saves. At ~60 µs network RTT the theoretical difference (2 × ~150 ns receive-side handle lifecycle vs 1 × ~200 ns resend string-decode ≈ net +100 ns/RTT in favor of B) is far below the ±5 µs run noise; B was never slower and trended faster at 8 B. The receive-side cost is paid on every delivered schema-carrying message whether or not it is ever re-sent, while the saving applies only to the resend.
Recommendation: drop the receive-side conditional handle (received encodings always value-only (id, schema)), keeping the send-side handle arm (custom-constructed encodings still resend by handle — construction is the one crossing). That reverts the delivery half of ZettaScaleLabs/zenoh-flat-jni#6 and simplifies the wire (getEncoding__handle leaf gone, no per-message native alloc/Cleaner churn for custom-encoding traffic). Awaiting go-ahead before implementing.
🤖 Generated with Claude Code
Adapts to zenoh-flat-jni aecffac, which dropped the receive-side conditional handle leaf: the ping-pong A/B (ZPing/ZPong with a custom schema-carrying encoding, real payload+encoding retransmission) showed the per-receive handle lifecycle costs more than the schema re-decode it saves on resend (flat medians 68/68us handle vs 58/67us value-only at 8B/1KB, value-only never slower). Custom encodings still own a construction-born handle and send as a bare jlong; a saved received encoding re-sends through the (id, schema) value arm. Callback lambdas and Sample/Query.fromParts lose the encH leaf; EncodingHandleTest asserts value-only delivery; CI pin bumped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xpr-string-receive # Conflicts: # .github/workflows/ci.yml # zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt # zenoh-java/src/commonMain/kotlin/io/zenoh/sample/Sample.kt # zenoh-java/src/jvmTest/kotlin/io/zenoh/EncodingHandleTest.kt
The custom-encoding echo instrumentation was measurement scaffolding for the Encoding handle A/B (verdict shipped: received encodings are value-only); the examples keep their upstream form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Consumes ZettaScaleLabs/zenoh-flat-jni#7: the receive path goes value-only — for KeyExpr (string-backed except declared) and for Encoding (always
(id, schema); a native handle exists only where it carries value: at construction of a custom encoding).KeyExpr: string-backed except declared
The KeyExpr ownership model becomes the Encoding model transplanted:
Session.declareKeyExpr→ the single case zenoh attaches a wire declaration (verified in zenoh source: the declaration is the only state aKeyExprholds beyond its string, and it pays off only on send through the declaring session). The handle is held untilundeclare, which demotes to string-backed.tryFrom→ validates natively once, releases the probe handle, keeps the string.autocanonize→ reads the canonized string, releases.getStrcrossing for every consumer that reads the string.KeyExpr.jniSel/jniStr/jniHandle(mirror of the Encoding helpers;cloneHandle()for consuming params); algebra ops use the declared handle or a transient probe.The class KDoc's "crucial to manually close" clause is again true only of declared instances — matching upstream semantics. Measured (
run_thr_java.sh): 8B +0.3%, 1KB +1.6% — neutral throughput with the per-message leak gone.Encoding: received encodings are value-only
To settle whether the receive-side conditional handle (flat-jni#6) paid for itself, its motivating receive-then-resend scenario was measured directly: a ping-pong where the ping carries a custom schema-carrying encoding and the pong retransmits the received payload + encoding (temporarily instrumented ZPing/ZPong — measurement scaffolding only, the committed examples keep their upstream form), driven by a new
run_ping_java.shsuite script (structure ofrun_thr_java.sh; median RTT µs):Value-only never slower — the per-receive handle lifecycle (clone + Box + wrapper + Cleaner, paid on every delivered schema-carrying message, twice per round trip) costs more than the schema re-decode it saves on the resent fraction (~200ns, once). So received encodings are now plain
(id, schema); a saved received encoding re-sends through the value arm. Custom encodings still own their construction-born handle and send as a bare jlong. Callback lambdas andSample/Query.fromPartslose theencHleaf; thejniSel/jniId/jniSchema/jniHandleselector props are unchanged.Tests
113 jvmTest, 0 failures — the suite plus
KeyExprHandleTest(constructed keyexprs string-backed with working algebra; declared ones own a handle until undeclared; received ones string-backed, equal to the sender's, reusable forput) and the adaptedEncodingHandleTest(presets value-only end-to-end; custom encodings own a construction handle; received encodings value-only and re-sendable).Sequencing
Requires ZettaScaleLabs/zenoh-flat-jni#7; CI pin points at its branch tip
e4a8ed7(bump to the merge commit after it lands). Basezenoh-flat-transition(with #488 squashed) is merged in; the value-only model in this PR supersedes #488's conditional-handle state.🤖 Generated with Claude Code