Files
amethyst/quartz/plans/2026-07-04-sync-serialization-and-ingest-latency.md
T
Claude b9d7ea2574 perf(nip77): direct-build NEG-MSG wire frames (~2.5–2.8× serialization)
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
2026-07-04 15:40:26 +00:00

61 lines
2.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.encode`d 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.