Skip to content

feat(pxe): constrained tag sync optimization and recipient logs sync benchmarks #24275

Open
vezenovm wants to merge 29 commits into
merge-train/fairies-v5from
mv/optimize-constrained-tag-sync
Open

feat(pxe): constrained tag sync optimization and recipient logs sync benchmarks #24275
vezenovm wants to merge 29 commits into
merge-train/fairies-v5from
mv/optimize-constrained-tag-sync

Conversation

@vezenovm

@vezenovm vezenovm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Fixes F-704

Summary

Constrained delivery emits a gapless tagging-index stream, so PXE can stop scanning at the first missing constrained tag instead of probing the full unfinalized window on every sync.

This PR:

  • Starts constrained scans with a small probe and doubles the probe size while every probed index has a log, capped at the existing window.
  • Keeps steady-state polling cheap: one tag per constrained secret.
  • Reduces catch-up round-trips geometrically until the probe reaches the window cap.
  • Decouples probe advancement from finalized-cursor persistence so unfinalized logs are still fetched without persisting unsafe cursors.
  • Leaves unconstrained sync and findHighestIndexes unchanged.

Benchmarks

Benchmarks compare doubling against fixed-size probes. The takeaway is that doubling preserves the steady-state tag-query floor of P=1 while avoiding P=1's one-round-trip-per-log catch-up behavior.

A sync runs in rounds: each round computes the next batch of tags, sends them to the node, and blocks on the result before deciding the next round. All counts are per sync.

  • tag-queries — total tags looked up on the node.
  • round-trips — sequential client waits. A round's tags are chunked into parallel calls internally, so a wide round is still one round-trip.
  • blocking-ms — measured wall-clock the caller spends blocked on the node with modeled node latency. This is reported only and varies run to run.

Scenarios seed a recipient that already synced prior messages, then measure the next single sync. secrets = N means N independent sender streams synced together in one batched pass.

  • steady-state — no new logs since the last sync.
  • catch-up-K — K new contiguous logs per secret since the last sync.
  • mixed — 999 idle secrets plus 1 deep straggler (K=100) at 1,000 secrets.

Cost per sync: doubling vs fixed-P alternatives

doubling is the shipped policy. The fixed-P columns are the selection comparison that motivated it: P=84 is the current full-window behavior, and P=1..5 sweep the constant-step alternative.

Unconstrained is not shown as its own row: its windowed scan cannot first-miss, so it is invariant to P and reproduces the P=84 column.

Tag-queries (thousands) (exact count = value x 1,000; bold = fewest among the fixed-P columns; the doubling column is the shipped policy):

scenario doubling P=1 P=2 P=3 P=4 P=5 P=84
steady, 100 0.1 0.1 0.2 0.3 0.4 0.5 8.4
steady, 1,000 1 1 2 3 4 5 84
catch-up-1, 100 0.3 0.2 0.2 0.3 0.4 0.5 8.4
catch-up-1, 1,000 3 2 2 3 4 5 84
catch-up-3, 100 0.7 0.4 0.4 0.6 0.4 0.5 8.4
catch-up-3, 1,000 7 4 4 6 4 5 84
catch-up-84, 100 12.7 8.5 8.6 8.7 8.8 8.5 16.8
catch-up-84, 1,000 127 85 86 87 88 85 168
catch-up-50, 100 6.3 5.1 5.2 5.1 5.2 5.5 8.4
catch-up-50, 1,000 63 51 52 51 52 55 84
catch-up-100, 100 12.7 10.1 10.2 10.2 10.4 10.5 16.8
catch-up-100, 1,000 127 101 102 102 104 105 168
mixed, 1,000 (999 idle + 1 deep K=100) 1.126 1.1 2.1 3.099 4.1 5.1 84.084

Round-trips (bold = fewest among the fixed-P columns; the doubling column is the shipped policy):

scenario doubling P=1 P=2 P=3 P=4 P=5 P=84
steady, 100 1 1 1 1 1 1 1
steady, 1,000 1 1 1 1 1 1 1
catch-up-1, 100 2 2 1 1 1 1 1
catch-up-1, 1,000 2 2 1 1 1 1 1
catch-up-3, 100 3 4 2 2 1 1 1
catch-up-3, 1,000 3 4 2 2 1 1 1
catch-up-84, 100 7 85 43 29 22 17 2
catch-up-84, 1,000 7 85 43 29 22 17 2
catch-up-50, 100 6 51 26 17 13 11 1
catch-up-50, 1,000 6 51 26 17 13 11 1
catch-up-100, 100 7 101 51 34 26 21 2
catch-up-100, 1,000 7 101 51 34 26 21 2
mixed, 1,000 (999 idle + 1 deep K=100) 7 101 51 34 26 21 2

Blocking wall-clock (ms) (reported only, noisy; the doubling column is from the committed bench, the fixed-P columns from the comparison sweep, so they are not from a single run):

scenario doubling P=1 P=2 P=3 P=4 P=5 P=84
steady, 100 6 6 6 6 6 6 8
steady, 1,000 6 7 5 7 7 8 21
catch-up-1, 100 14 13 8 8 8 8 12
catch-up-1, 1,000 32 32 27 29 31 28 54
catch-up-3, 100 24 29 18 18 12 12 15
catch-up-3, 1,000 76 83 73 79 64 67 95
catch-up-84, 100 214 663 407 335 321 265 205
catch-up-84, 1,000 1,838 2,226 1,942 1,935 1,871 1,830 2,000
catch-up-50, 100 135 397 243 193 171 161 127
catch-up-50, 1,000 1,109 1,307 1,169 1,191 1,114 1,108 1,217
catch-up-100, 100 240 784 480 396 348 321 245
catch-up-100, 1,000 2,203 2,632 2,348 2,341 2,212 2,236 2,366
mixed, 1,000 (999 idle + 1 deep K=100) 41 590 296 199 150 124 32

Takeaway

P=1 is the tag-query floor, but it pays one round-trip per new log during catch-up. The full window (P=84) minimizes catch-up round-trips, but it charges every idle secret the full-window tag cost on every sync.

Doubling is the middle ground: it matches P=1 at steady state, stays close to P=1 on tag queries during catch-up, and collapses deep catch-up round-trips geometrically. In the mixed scenario, doubling uses 1,126 tag queries vs 1,100 for P=1, but needs 7 round-trips instead of 101; compared with P=84, it avoids the 84,084-tag idle tax while staying within 5 round-trips of the full-window catch-up path.

Testing

  • Unit tests cover scan shape, doubling behavior, batched-round semantics, and returned-log equivalence.
  • Benchmark tests report tag queries, round-trips, and modeled blocking time for steady-state, catch-up, mixed, and unconstrained scenarios.

Constrained delivery emits a gapless tagging-index stream, so the recipient can stop at the first missing tag instead of always probing a full UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN (20) window per secret. The initial probe is INITIAL_CONSTRAINED_PROBE_LEN tags and grows to the full window only when every probed index has a log; probe advancement is decoupled from finalized-cursor persistence so unfinalized logs at the top of the run are still fetched. Adds a unit micro-benchmark for the per-sync tag-query cost.
@vezenovm vezenovm added the ci-draft Run CI on draft PRs. label Jun 24, 2026
@vezenovm vezenovm changed the title perf(pxe): first-miss probing for constrained tag sync feat(pxe): first-miss probing for constrained tag sync Jun 24, 2026
@vezenovm vezenovm changed the title feat(pxe): first-miss probing for constrained tag sync feat(pxe): first-miss probing for constrained tag sync and recipient logs sync benchmarks Jun 24, 2026
@vezenovm vezenovm changed the title feat(pxe): first-miss probing for constrained tag sync and recipient logs sync benchmarks feat(pxe): constrained tag sync optimization and recipient logs sync benchmarks Jun 24, 2026
vezenovm added 13 commits June 24, 2026 16:04
The mocked node makes sync-time an unfaithful end-to-end latency; real full-sync timing already lives in the client_flows /sync e2e bench (ProvingTimings.sync). Keeps the metrics the micro-bench is for: tag-queries, round-trips, and blocking-time.
Add a realistic mixed-population row (999 idle + 1 deep straggler at K=100)
and run deep catch-up at 1000 secrets as well as 100, so every value in the
PR-description tables is produced by the committed bench. Generalize scenario
seeding and assertions to a per-secret new-log distribution and compute
firstMissOptimum from it.
The constrained tag-sync source restated "gapless stream stops at the
first missing tag" ~5x and the doubling-probe mechanics ~4x. Keep one
canonical home for each (the module JSDoc for the why, the nextProbeLen
comment for the mechanics) and reduce the rest to one-line local facts;
also correct boundEnd's comment to note it is an exclusive bound and
probeLen's to note the queried span can be smaller when boundEnd caps it.

Drop the tag-sync micro-bench from bench_cmds: it existed to find the
probe-growth numbers locally, which is done, so we no longer track it on
the benchmark dashboard. It stays runnable as a local tool, and the
doubling behavior remains covered in CI by the unit-test file.
Revert the in-place strengthening of the existing unconstrained and mixed
tests and re-express the constrained-specific coverage as three new tests in
the constrained-secrets block: multi-secret batching, halting at the first
all-miss batch (sentinel proof), and doubling-probe re-anchoring past the
cold-start bound.
The constrained-delivery optimization rewrote the old "continues when all indexes in the batch have logs" test (which scanned the full window every round) into the constrained doubling-probe test, leaving the unconstrained full-window-scan drain of a run longer than one window without direct coverage. Add an unconstrained test pinning that a contiguous run filling and exceeding the cold-start window drains fully: all logs returned, both cursors advanced to the last index, and per-round query sizes of [WINDOW_LEN + 1, WINDOW_LEN] showing full-window re-anchoring rather than a doubling probe.
Comment thread yarn-project/pxe/src/tagging/recipient_sync/sync_tagged_private_logs.test.ts Outdated
Comment thread yarn-project/pxe/src/tagging/recipient_sync/sync_tagged_private_logs.test.ts Outdated
@vezenovm vezenovm added the ci-full Run all master checks. label Jun 26, 2026
vezenovm added 6 commits June 26, 2026 16:26
The constrained-sync micro-benchmark is skipped in CI (test_cmds skips
*.bench.test.ts and it is not in bench_cmds), so bring its coverage into
sync_tagged_private_logs.test.ts, which runs in CI:
- log-then-linear round-count law (K=1,3,20,50,100 -> 2,3,5,6,9 rounds)
- batched straggler: a single deep straggler among idle secrets sets the
  round count while idle secrets each cost one probe and drop
- cold-start constrained-vs-unconstrained scan-shape contrast
- tighten the deep-cap test to pin the linear-past-cap regime

Qualify the overstated ~log2(K) catch-up claim in constants.ts,
sync_tagged_private_logs.ts, and the bench: catch-up is logarithmic only
while the probe doubles, then linear at ~K/WINDOW_LEN once it saturates the
cap. No behavior change.
vezenovm added 5 commits July 9, 2026 12:21
Merge the constrained drain test into the boundEnd re-anchor test, move the
unconstrained drain next to its constrained counterpart, and drop the
steady-state full-window test subsumed by the store-index test. Strengthen the
unfinalized-probe test with a second-sync restart assertion and make the probe
reset test saturate the WINDOW_LEN cap first. Remove the bench's probe-schedule
mirror (behavior is pinned by the CI unit tests) and switch remaining
"cursor" wording to "finalized index".
@vezenovm vezenovm removed the ci-draft Run CI on draft PRs. label Jul 9, 2026
@vezenovm vezenovm marked this pull request as ready for review July 9, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-full Run all master checks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant