Skip to content

fix: reduce hot-path allocation, cloning, and lock contention#524

Open
varex83agent wants to merge 1 commit into
mainfrom
perf/hot-path-alloc-clone-reduction
Open

fix: reduce hot-path allocation, cloning, and lock contention#524
varex83agent wants to merge 1 commit into
mainfrom
perf/hot-path-alloc-clone-reduction

Conversation

@varex83agent

Copy link
Copy Markdown
Collaborator

Summary

Implements the eight hot-path optimizations from the hot-path allocation, cloning, and lock-contention reduction plan. Every change is perf-only and preserves functional equivalence with Charon v1.7.1 — no wire format, validation, ordering, or metric semantics change.

Tasks

Task Crate Change
T01 p2p Add alloc-free PeerStore::has_connection; migrate the 5 emptiness-only connections_to_peer(..).is_empty() callers (qbft, frostp2p, bcast, parsigex, monitoringapi).
T02 consensus (qbft) Thread one Arc<QbftConsensusMsg> across all peers on broadcast instead of an N-1 deep clone of the multi-MB payload; move msg/justification/values out of pb_msg on receive; values_by_hash now consumes Vec<Any>.
T03 eth2api Arc-wrap the cached ActiveValidators/CompleteValidators maps (cheap clones via Deref), add a read-lock fast path on a warm cache, and release the write lock across the beacon-node fetch (re-checking on re-acquire).
T04 core (scheduler) Replace the O(N) to_entries() status-metric scan with a per-pubkey last-status map, zeroing only the previously-reported series — mirrors Charon's statusGauge.Reset.
T05 core (scheduler) Cache slots_per_epoch on the actor at build time instead of a beacon-node round-trip on every duty lookup.
T06 parsigex Acquire the peer-store guard once per broadcast (using T01's has_connection) and share the encoded payload via Arc<[u8]> instead of a per-target Vec<u8> copy.
T07 core (parsigdb) Fold the threshold check into store under the lock and return only the matched set via a StoreOutcome enum, dropping the per-store full-accumulator deep clone.
T08 ssz Pass the buffer by value into merkleize_impl (one copy instead of two); reuse a single Sha256 in default_hash_fn; drop the put_bitlist scratch clone.

Tests

New unit/regression tests added for each task (peer-store connectivity, Arc::ptr_eq shared-buffer assertions for qbft and parsigex, concurrent valcache misses, per-pubkey status-metric transitions, cached-slots_per_epoch epoch math, parsigdb duplicate/mismatch paths, and ssz golden merkleization roots).

Quality gates

  • cargo +nightly fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features ✅ (all suites pass)
  • cargo deny check

🤖 Generated with Claude Code

Implements the eight hot-path optimizations from the "hot-path allocation,
cloning, and lock-contention reduction" plan. All changes are perf-only and
preserve functional equivalence with Charon v1.7.1.

- T01 p2p: add alloc-free `PeerStore::has_connection`; migrate the five
  emptiness-only `connections_to_peer(..).is_empty()` call sites.
- T02 qbft: thread one `Arc<QbftConsensusMsg>` across peers on broadcast
  instead of an N-1 deep clone; move `msg`/`justification`/`values` out of
  `pb_msg` on receive; `values_by_hash` consumes its input.
- T03 eth2api: `Arc`-wrap the cached validator maps, add a read-lock fast
  path, and release the write lock across the beacon-node fetch (re-checking
  on re-acquire).
- T04 scheduler: replace the O(N) `to_entries()` status-metric scan with a
  per-pubkey last-status map, zeroing only the previously-reported series.
- T05 scheduler: cache `slots_per_epoch` on the actor at build time instead
  of a beacon-node round-trip on every duty lookup.
- T06 parsigex: acquire the peer-store guard once per broadcast and share the
  encoded payload via `Arc<[u8]>` instead of a per-target `Vec<u8>` copy.
- T07 parsigdb: fold the threshold check into `store` under the lock and
  return only the matched set, dropping the per-store full-accumulator clone.
- T08 ssz: pass the buffer by value into `merkleize_impl` (one copy, not two);
  reuse one `Sha256` in `default_hash_fn`; drop the `put_bitlist` clone.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
@varex83 varex83 changed the title perf: reduce hot-path allocation, cloning, and lock contention fix: reduce hot-path allocation, cloning, and lock contention Jul 6, 2026

@emlautarom1 emlautarom1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, some small nits only.

Comment on lines +373 to +374
// Move the validated parts out of `pb_msg` (it is dropped after this),
// avoiding a clone of the message, justifications, and values.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, these AI comments do not provide much value. They explain what is immediately happening in the code. Unless the code is not clear then prefer to drop it. The comments will eventually get stale and add confusion later.

Comment on lines +188 to +190
// Wrap once so the fan-out shares a single allocation across all
// peers rather than deep-cloning the (potentially multi-MB)
// payload per target, matching Charon's shared-pointer broadcast.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example: here, the fact that we're using an Arc is required due to the BroadcastCommand signature. If you need to justify its usage do it on the type definition itself, or just drop it.

/// `statusGauge.Reset(pubkey, ...)` + `Set(1)` in O(1) per validator: it zeroes
/// only the previously-reported series for this pubkey (when the status
/// changed) and sets the current one to 1.
fn submit_validator_status_metric(pubkey: &types::PubKey, status: &str) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should explore if we can make this a feature in Vise itself. We already are referencing it from Git.

Comment on lines +209 to +212
// Fetch the chain constants once at build time (the node is synced at
// this point) and cache `slots_per_epoch` on the actor, mirroring
// Charon's memoized-spec behaviour.
let (_slot_duration, slots_per_epoch) = client.api().fetch_slots_config().await?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should explore a more general solutions: there are beacon client calls that are expected to not change throughout the lifetime of the application (ex. fetch_slots_config), so we could cache them at the client level.

For now this is fine, but there are multiple places where this function is invoked and we'll end up adding caching code to each one of them.

Comment on lines +147 to +149
let (eth2_cl, pubkeys) = {
let inner = self.0.read().await;
(inner.eth2_cl.clone(), inner.pubkeys.clone())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could refactor this and extract client and pubkeys from inner: these values do not change so no need to put them under the lock.

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.

2 participants