The server's per-round reconcile spends a large slice turning the ~1MB hex reconcile frame into wire JSON: the generic (Jackson) serializer wraps the hex string in a value node and scans every char for JSON escapes a [0-9a-f] payload can never contain, then re-copies. NegMsgMessage.toJson() now builds ["NEG-MSG","<sub>","<hex>"] directly — no node tree, no escape scan of the hex. Fast path fires only for escape-free printable-ASCII subIds (what the JSON encoder emits verbatim); exotic subIds fall back to the generic serializer, so output is byte-identical. RelaySession.send routes through message.toJson() (default unchanged for every other message type). Measured (toJson + UTF-8, per frame): 64KiB 2.5×, 250KiB 2.6×, 500KiB (strfry cap) 2.8× — ~2.5ms saved per NEG-MSG, ~35ms over a 14-round reconcile. Correctness: a subId battery asserts byte-identity with the generic path, and GeodeVsStrfryNegentropySyncTest (real strfry) reconciles against the fast-built frames. Also records the ingest-latency candidate as measured-not-worth-it: the IngestQueue pipeline overhead is only ~0.17ms p50, <10% of the ~2.4ms receipt→queryable gap — that gap lives in the REQ-visibility path, not the writer. Full write-up in quartz/plans/2026-07-04-sync-serialization-and-ingest-latency.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EZeWww5TJnzBZKPoc6mvU
2.9 KiB
Two more relayBench gaps: NEG-MSG serialization (fixed) + ingest latency (measured, not worth it)
Follow-up to the 1M relayBench run and the NIP-77 diagnosis
(2026-07-04-negentropy-reconcile-profiling.md). Both were measured in
isolation first so the fix could be judged on the delta.
NEG-MSG wire serialization — fixed (~2.5–2.8×)
The server's per-round reconcile JFR put a large slice in serialization: a
reconcile frame is Hex.encoded to a ~1 MB hex string, then the outgoing
NegMsgMessage was turned into wire JSON by the generic (Jackson)
serializer, which wraps the giant hex string in a value node and scans
every char for JSON escapes a [0-9a-f] payload can never contain, then
re-copies.
NegMsgMessage.toJson() now builds ["NEG-MSG","<sub>","<hex>"] directly —
no node tree, no escape scan of the hex. The fast path fires only when the
client-chosen subId is escape-free printable ASCII (the bytes the JSON
encoder emits verbatim); any exotic subId (quotes, control chars, non-ASCII)
falls back to the generic serializer, so output is byte-identical.
RelaySession.send now routes through message.toJson() (default is
unchanged for every other message type).
Measured (NegMsgSerializationBenchmark, toJson + UTF-8, per frame):
| frame | generic | fast | speedup |
|---|---|---|---|
| 64 KiB | 0.47 ms | 0.19 ms | 2.5× |
| 250 KiB | 1.97 ms | 0.76 ms | 2.6× |
| 500 KiB (strfry cap) | 3.87 ms | 1.37 ms | 2.8× |
At the 500 KB frame cap that's ~2.5 ms saved per NEG-MSG, ~35 ms over a
14-round reconcile — server-side, on top of the (library-side) prefix-sum
fingerprint work. Correctness: a subId battery asserts byte-identity with
the generic path, and the GeodeVsStrfryNegentropySyncTest interop test
(real strfry) reconciles against the fast-built frames.
Receipt➜queryable ingest latency — measured, not worth fixing
Hypothesis: geode's group-commit IngestQueue (two channel handoffs,
submit→verifier→writer) adds latency for a single event on an idle relay,
explaining the 1M gap (geode 4.68 ms vs strfry 2.32 ms p50).
IngestLatencyBenchmark timed submit→onComplete (fires after COMMIT =
queryable) vs a direct batchInsert:
| path | p50 | p90 | p99 |
|---|---|---|---|
| IngestQueue (submit→OK) | 0.21 ms | 0.31 ms | 0.50 ms |
| direct batchInsert | 0.05 ms | 0.09 ms | 0.17 ms |
| pipeline overhead | 0.17 ms |
The whole ingest trip is ~0.2 ms — the pipeline overhead (~0.17 ms) is <10%
of the ~2.4 ms gap. A single-event fast path in the (carefully-tuned) writer
can't close it. The gap is in the cross-connection REQ-visibility/poll
path, not ingest — the probe publishes on one connection and hammer-polls
REQ {ids:[id]} on another, so it's dominated by websocket round-trips +
how fast a committed row surfaces to a concurrent reader, not the write. A
real fix needs that path profiled; the writer is not the lever. Benchmark
kept as the evidence.