Skip to content

Receive path goes value-only: KeyExpr string-backed except declared, Encoding always (id, schema)#489

Merged
milyin merged 9 commits into
zenoh-flat-transitionfrom
keyexpr-string-receive
Jul 17, 2026
Merged

Receive path goes value-only: KeyExpr string-backed except declared, Encoding always (id, schema)#489
milyin merged 9 commits into
zenoh-flat-transitionfrom
keyexpr-string-receive

Conversation

@milyin

@milyin milyin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 a KeyExpr holds beyond its string, and it pays off only on send through the declaring session). The handle is held until undeclare, which demotes to string-backed.
  • tryFrom → validates natively once, releases the probe handle, keeps the string. autocanonize → reads the canonized string, releases.
  • Received keyexprs (sample/query/reply) → one eager string. Zenoh's RX path never attaches a declaration, so the old per-message handle bought nothing on re-send while costing an unfreeable native allocation per message (gc-managing it measured −23% thr) plus a second getStr crossing for every consumer that reads the string.
  • Sends pick the string/handle selector arm via 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.sh suite script (structure of run_thr_java.sh; median RTT µs):

flat×flat median RTT handle delivery value-only
8 B 68 µs 58 µs
1 KB 68 µs 67 µ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 and Sample/Query.fromParts lose the encH leaf; the jniSel/jniId/jniSchema/jniHandle selector 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 for put) and the adapted EncodingHandleTest (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). Base zenoh-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

milyin and others added 3 commits July 17, 2026 11:56
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>
@milyin

milyin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Benchmark results (run_thr_java.sh, CROSS=0, 3 passes × 8 rounds × 100k msgs, back-to-back runs)

flat×flat subscriber medians, baseline = pre-KeyExpr state (flat-jni main 1e040c9 + branch encoding-conditional-handle):

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>
@milyin

milyin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: with the hot path now handle-free, flat-jni's KeyExpr became .gc_managed() (ZettaScaleLabs/zenoh-flat-jni#7 2d58775) — declared keyexprs get the Cleaner leak backstop at zero per-message cost. CI pin bumped; 113 jvmTest green unchanged.

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>
@milyin

milyin commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Encoding handle A/B: ping-pong with real receive-then-resend (run_ping_java.sh, new suite script)

ZPing/ZPong were fixed first (ec7521d): every ping carries a custom schema-carrying encoding (application/custom;zping-schema), ZPong retransmits the received payload and encoding, and ZPing verifies the encoding round-trips — so each RTT contains 2 receive-side deliveries of a custom encoding plus 1 resend-of-received (pong side).

A = current code (received schema-carrying encoding carries a native handle; resend by jlong). B = wire-identical flip (encoding_if_schemaNone, uncommitted): received encodings value-only; resend re-decodes the schema string. 3 passes × 1000 RTTs, flat×flat medians:

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>
@milyin milyin changed the title KeyExpr: string-backed except declared Receive path goes value-only: KeyExpr string-backed except declared, Encoding always (id, schema) Jul 17, 2026
milyin and others added 3 commits July 17, 2026 19:07
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>
@milyin
milyin merged commit 6fa73b8 into zenoh-flat-transition Jul 17, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant