starknet_patricia_storage: flatten mget chunk keys into one buffer - #15004
starknet_patricia_storage: flatten mget chunk keys into one buffer#15004gkaempfer wants to merge 2 commits into
Conversation
Each blocking read task in RocksDbStorage::mget cloned every key's Vec<u8> individually, allocating once per key. On a large witness fetch (100k+ keys) that is tens of thousands of small heap allocations per mget call. Concatenate a chunk's key bytes into one buffer plus (start, end) spans instead, so each task moves into spawn_blocking with two allocations total rather than one per key.
Clarify flatten_keys' doc comment on span semantics (half-open, zero-length keys), and add test_mget_varying_key_lengths to cover a chunk mixing key lengths including an empty key, mutation-verified against a corrupted span.
PR SummaryLow Risk Overview Query semantics and result ordering are unchanged; slices are rebuilt inside the closure from the flattened buffer. Adds Reviewed by Cursor Bugbot for commit 48632e6. Bugbot is set up for automated code reviews on this repo. Configure here. |
Problem
Follow-up to #15000, which split
RocksDbStorage::mgetacrossspawn_blockingtasks for read parallelism on the committer's per-block Patricia witness-fetch hot path (sometimes 100,000+ keys in one call).Each task built its chunk of keys as
Vec<Vec<u8>>by individually cloning every key'sVec<u8>:That's one heap allocation per key, purely to give
spawn_blocking's'staticclosure owned data — on a 100k-key witness fetch, tens of thousands of small allocations permgetcall.Fix
flatten_keysconcatenates a chunk's key bytes into oneVec<u8>buffer plus aVec<(usize, usize)>of(start, end)byte spans (sized exactly via a single pass, soVec::with_capacitynever reallocates). Eachspawn_blockingtask now moves in two allocations total instead of one per key; the byte slices are reconstructed from the buffer inside the closure body, after the move, so there's no self-referential-struct concern.This only touches how the chunk's keys travel into the blocking task — it doesn't change what's queried or the order values come back in.
Scope note (from review)
rust-rocksdb0.44.1'smulti_get_optstill copies each key into its ownBox<[u8]>internally before calling into the C API, so this change takes per-key allocations from 2 down to 1 (our clone + RocksDB's own), not to zero. Getting rid of the remaining one would mean switching tobatched_multi_get_cf_opt, which hands RocksDB raw pointers into the caller's buffer instead — a larger change (needs aColumnFamilyhandle, returnsDBPinnableSlice) left as a possible follow-up rather than folded in here.Testing
RUSTC_WRAPPER="" cargo test -p starknet_patricia_storage --features rocksdb_storage,mdbx_storage— 13/13 pass, including the existingtest_mget_preserves_key_order(both chunked and single-task) and a newtest_mget_varying_key_lengthscovering a chunk that mixes key lengths (1–7 bytes) plus a zero-length key, which would surface any off-by-one in the span bookkeeping. Mutation-checked: corrupting a span's end fails both tests.Reviewed by an Opus subagent for correctness (span math, closure/lifetime soundness, code-style compliance) before opening this PR; clippy and
rust_fmt.share clean.Related: #15000
Generated by Claude Code