Skip to content

rendezvous: faster snapshot saves (hand-written encoder + trust-pairs cache) - #131

Merged
TeoSlayer merged 5 commits into
mainfrom
perf/cache-trust-pairs-encode
Oct 1, 2026
Merged

TeoSlayer merged 5 commits into
mainfrom
perf/cache-trust-pairs-encode

Conversation

@TeoSlayer

@TeoSlayer TeoSlayer commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Problem

flushSave re-encodes the entire registry snapshot every ~5s. At fleet scale (~218k nodes, 8.3M trust pairs, ~191 MB JSON) the encode was the dominant cost: encoding/json reflects over the whole graph and allocates heavily (profiling showed appendString 7.7s + mallocgc 11s + findObject 6s per window — i.e. a GC storm), all single-threaded.

This PR makes the encode much cheaper while producing byte-identical output — the snapshot checksum and load() recovery are unchanged.

Changes

  1. Hand-written top-level encoder — wal.AppendSnapshot (with SnapshotNodes):

    • Nodes and PubKeyIdx are encoded directly (sorted keys, exact field order/omitempty) with a fast string escaper (falls back to encoding/json only for non-ASCII/control/quote/backslash strings).
    • TrustPairs is emitted verbatim from a cached json.RawMessage.
    • Small/rare fields delegate to encoding/json per value so their bytes are exactly right.
    • flushSave encodes into a reused buffer, hashes, and writes body + checksum trailer atomically.
  2. Trust-pairs encode cache — a revision on the trust-pair set (bumped only on real add/remove) plus a 10-minute TTL safety net; empty → nil so omitempty still drops the field.

  3. Funky-string hardening — explicit differential test stuffing every string field and map key with quotes, backslashes, HTML, control chars, U+2028/2029, unicode, emoji, invalid UTF-8, very long and whitespace values; plus fuzz seeds for the same.

Parity & tests

  • Byte-identical to json.Encoder(SetEscapeHTML(false)) — asserted by differential tests and three native fuzz targets (FuzzAppendJSONString, FuzzSnapshotNodes_MarshalParity, FuzzAppendSnapshot_MarshalParity; ~44M executions, zero mismatches).
  • Full module suite green.

Measured (on the actual registry box under real load, Xeon 2.2 GHz)

Nodes encode:  encoding/json 1350ms / 74MB  ->  hand-written 576ms / 13MB   (2.35×)
M3:            ~150ms          / 74MB         ->  ~66ms        / 13MB        (2.25×)

Parallel chunking was tried and reverted — memory-bandwidth-bound, no faster, ~18× more allocation.

Notes

  • No cadence change: the 5s save tick and full-state snapshot are untouched; this only makes each save cheaper.
  • Accompanying infra (not in this PR): store moved 50 GB pd-standard → 1 TB pd-ssd (fsync 3.7 MB/s → ~250–350 MB/s).
  • Follow-up (not included): incremental per-node fragment caching (only re-encode changed nodes) and WAL coverage for trust/hostname/tags/visibility to enable a longer checkpoint cadence.

pilot added 2 commits October 1, 2026 22:24
flushSave re-encoded the entire trust-pair list (millions of "min:max"
strings at fleet scale) on every save, single-threaded — the dominant cost
of snapshot encoding. The set is near-static in practice, so cache its JSON
array and reuse it.

- trust: trustPairSet gains a revision counter, bumped only on real
  add/remove; trust.Store.Revision() exposes it.
- server: trustPairsEncodeCache memoizes the JSON array keyed by revision,
  with a 10-minute TTL safety net so a missed invalidation can't wedge a
  stale list. Empty list still returns nil so omitempty drops the field.
- Snapshot.TrustPairs becomes json.RawMessage (emitted verbatim); load and
  replication decode it back to []string.

Output is byte-identical to encoding the slice directly (tested), so the
snapshot checksum and recovery semantics are unchanged. No behavioural
change: the 5s save cadence and full-state snapshot are untouched.

Tests: byte-equality vs encoding/json, revision invalidation, TTL expiry,
empty->nil, and pairset revision semantics. Full module suite green.
200k nodes + 5M trust pairs: ~315ms -> ~203ms per snapshot encode
(~35% faster), with ~344MB less allocated per save.
@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

pilot added 3 commits October 2, 2026 00:55
…tical)

flushSave encoded the snapshot with encoding/json, which reflects over the
whole 200k-node graph every save and allocates heavily (the dominant cost
plus a GC storm at fleet scale).

wal.AppendSnapshot is a hand-written top-level encoder:
- Nodes and PubKeyIdx are encoded directly (map keys sorted, field order and
  omitempty mirrored) with a fast string escaper (falling back to
  encoding/json only for non-ASCII/control/quote/backslash strings).
- TrustPairs is emitted verbatim from its cached RawMessage.
- Small/rare fields delegate to encoding/json per value so their bytes are
  exactly right.

flushSave now encodes into a reused buffer via AppendSnapshot, hashes, and
writes body + checksum trailer atomically. Output is byte-identical to
encoding/json (differential test + native fuzzing), so the snapshot
checksum and load() recovery are unchanged.

Bench (200k nodes): ~113ms -> ~64ms, 325MB -> 13MB allocated. Full
cache-heavy case ~2.8x. Fuzzed: FuzzAppendJSONString,
FuzzSnapshotNodes_MarshalParity, FuzzAppendSnapshot_MarshalParity (~31M
execs, zero mismatches).
Chunking the node-map encode across goroutines measured no faster than
serial (the encoder is memory-bandwidth-bound) while allocating ~18x more
(172MB vs 9.4MB per 200k-node encode). Keep the serial hand-written encoder:
2.25x faster than encoding/json including the key sort, with ~6x less
allocation.
Add an explicit differential test stuffing every string field (and map keys)
with adversarial values — quotes, backslashes, HTML, control chars,
U+2028/2029, unicode, emoji, invalid UTF-8, very long, whitespace — plus
fuzz seeds for the same. Byte-parity with encoding/json holds; output is
always valid JSON.
@TeoSlayer TeoSlayer changed the title rendezvous: cache the trust-pairs snapshot encoding (revision + TTL) rendezvous: faster snapshot saves (hand-written encoder + trust-pairs cache) Oct 1, 2026
@TeoSlayer
TeoSlayer enabled auto-merge (squash) October 1, 2026 22:06
@TeoSlayer
TeoSlayer merged commit 00400aa into main Oct 1, 2026
8 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