Skip to content

perf(rpc): eliminate the full account scan from indexer-blobs via single-block replay - #498

Draft
ezeike wants to merge 6 commits into
mainfrom
feat/fsmhooks-simplified
Draft

perf(rpc): eliminate the full account scan from indexer-blobs via single-block replay#498
ezeike wants to merge 6 commits into
mainfrom
feat/fsmhooks-simplified

Conversation

@ezeike

@ezeike ezeike commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

The indexer-blobs endpoint previously computed its account delta by prefix-scanning the entire account set (~1.35M accounts) at both the current and previous heights and diffing the results — the dominant cost of every cache-miss request. This PR sources the account delta by replaying the single block that produced the requested height instead, plus a set of perf/observability improvements to the same endpoint.

Account-delta fast path

  • fsm: AccountChangeCollector classifies every account touched during one ApplyBlock call into added/changed/removed against the height-1 baseline (captured on first touch, before the block's own writes shadow it). ApplyBlock gains collector/skipRoot params — used only by throwaway replays, never by a real commit.
  • fsm: IndexerBlob gains skipAccounts; AssembleAccountDeltaSides rebuilds the wire-ready sides, reproducing the reward/slash force-include and ascending-address ordering the full scan provided for free.
  • controller: GetAccountDelta replays the block against a TimeMachine snapshot, restoring the root DEX cache from the committed certificate exactly as the live commit path does (required for nested-chain correctness).
  • rpc: IndexerBlobsCached sources account deltas from the fast path. Height 2 keeps the full-scan fallback so its response stays byte-identical (there is no previous height to diff against).

Perf / observability

  • Sorted merge walk replaces the map-diff in DeltaIndexerBlobs
  • Cold-read visibility: log + canopy_indexer_blob_cold_read_time for cache-miss reads that outlive the RPC write deadline
  • Cancel in-flight scans on client disconnect instead of computing and caching a response nobody is waiting for
  • Pebble LSM-tree metrics, previous-blob reuse counters, and per-step timing for delta compute/marshal

Testing

  • Differential regression test pinning the fast path byte-for-byte, order-for-order to the old full-scan-and-diff on the same committed chain
  • Force-include coverage for reward/slash accounts the block never writes
  • Prefix-leak, replay-QC-variant, and pool/validator-touching regression tests
  • go build ./... and fsm/controller/cmd/rpc suites pass with -count=1

🤖 Generated with Claude Code

ezeike and others added 6 commits August 4, 2026 17:13
changedBlobKeys diffed accounts/pools/validators by building a
map[string][]byte per side and comparing via map lookups - two full
hash-map builds per indexer-blobs request at ~1.35M accounts.

Add mergeChangedBlobKeys, a two-pointer merge walk over the already
Pebble-sorted entry slices, and use it for accounts and validators,
whose extracted keys (account/validator address bytes) match their
Pebble storage-key order exactly.

Pools stay on the map-based diff: poolEntryKey extracts Pool.Id's raw
varint wire bytes, which diverges from KeyForPool's big-endian storage
encoding at varint length boundaries (e.g. id 16383 vs 16384), so entry
order there does not reliably match key order.

Adds TestMergeChangedBlobKeys_MatchesMapBasedDiff, which asserts the
merge-walk output equals the original map-based diff output on the
same scenario, plus an empty-previous edge case.
cold_read_time was running ~3s higher than what current+previous IndexerBlob's
own instrumented steps accounted for (even after weighting accounts_iterate/
validators_iterate/block_non_signers_get by the previous-reuse miss rate) --
DeltaIndexerBlobs (parsing every account/pool/validator entry into a map on
both sides to diff them) and the final lib.Marshal call were both inside the
coldStart timing window but had no ObserveIndexerBlobStep of their own,
making them an invisible chunk of the total.

Adds delta_compute and delta_marshal steps to canopy_indexer_blob_step_time
so that gap is attributable instead of inferred.
…gle-block replay

Replace the full account prefix scan of both heights in IndexerBlobsCached
with a per-block account delta computed by replaying the single block that
produced the requested height:

- fsm: AccountChangeCollector classifies every account touched during one
  ApplyBlock call into added/changed/removed against the height-1 baseline;
  ApplyBlock gains collector/skipRoot params (replay-only, never commit)
- fsm: IndexerBlob gains skipAccounts; AssembleAccountDeltaSides rebuilds
  the wire-ready sides with the reward/slash force-include and ascending
  address order the full scan provided for free
- controller: GetAccountDelta replays the block against a TimeMachine
  snapshot, restoring the root DEX cache from the committed certificate as
  the live commit path does
- rpc: IndexerBlobsCached sources account deltas from the fast path; height
  2 keeps the full-scan fallback to stay byte-identical
- tests: differential test pinning fast-path output byte-for-byte to the
  old full-scan-and-diff, plus force-include, prefix-leak, and replay
  regression coverage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ezeike ezeike changed the title Feat/fsmhooks simplified perf(rpc): eliminate the full account scan from indexer-blobs via single-block replay Aug 4, 2026
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