Skip to content

perf(l1): adaptive request sizing, storage bisection, and parallel trie in snap sync#6181

Open
ilitteri wants to merge 2 commits intosnap-sync-phase1-optimizationsfrom
snap-sync-tier2-optimizations
Open

perf(l1): adaptive request sizing, storage bisection, and parallel trie in snap sync#6181
ilitteri wants to merge 2 commits intosnap-sync-phase1-optimizationsfrom
snap-sync-tier2-optimizations

Conversation

@ilitteri
Copy link
Collaborator

Motivation

Follow-up to #6178 (Tier 1: WAL disable, multi-peer healing, async select). Profiling Tier 1 results on Hoodi showed remaining bottlenecks in fixed-size network requests, sequential big-account storage downloads, and single-threaded account trie construction. Nethermind uses adaptive request sizing, range bisection, and parallel trie BulkSet to address these.

Description

Two commits implementing three Tier 2 optimizations:

1. Adaptive request sizing (5efae33)

  • Add RequestSizerMap — per-peer response_bytes tuning based on response latency
  • Start at 128KB, scale up 1.5x when latency < 2s, scale down 1.5x when > 10s, clamped to [50KB, 2MB]
  • Applied to all 5 snap protocol request types: account ranges, storage ranges, trie nodes (state + storage healing), and bytecodes
  • Includes 7 unit tests

2. Storage range bisection (5efae33)

  • When a big-account sub-range returns incomplete, bisect the remaining range into two sub-tasks instead of one
  • Both halves are dispatched via the normal task queue to potentially different peers, doubling download bandwidth
  • Minimum range threshold (size >= 2) prevents infinite subdivision
  • Composes with existing density-based big-account splitting logic

3. Parallel 16-way account trie construction (fb91a34)

  • Partition sorted account iterator into 16 buckets by first nibble of H256 key
  • Build each subtrie in parallel using std::thread::scope (16 scoped threads)
  • Merge 16 subtrie root hashes into a single root BranchNode
  • Refactor trie_from_sorted_accounts into _inner + finalize_root layers for reuse
  • Falls back to sequential when 0-1 nibble buckets are non-empty
  • Includes 5 new parallel construction tests

Benchmark results (Hoodi, ethrex-mainnet-4: 12 cores, 62 GB RAM)

Phase main (baseline) This PR (Tier 1+2) Delta
1. Block Headers 1:10 1:40 +30s (network)
2. Account Ranges 0:10 0:20 +10s (network)
3. Account Insertion 0:30 0:20 -10s (33%)
4. Storage Ranges 2:40 2:00 -40s (25%)
5. Storage Insertion 4:20 4:30 +10s (noise)
6. State Healing 0:10 0:10
7. Storage Healing 0:30 0:20 -10s
8. Bytecodes + finalize 3:41 2:10 -91s (41%)
Total 791s (13:11) 691s (11:31) -100s (13%)

Phases 1, 2 are network-bound (peer variability at startup). CPU/IO phases show consistent improvements:

  • Account Insertion: 33% faster from 16-way parallel trie construction
  • Storage Ranges: 25% faster from adaptive sizing + bisection improving network utilization
  • Bytecodes: 41% faster from adaptive sizing scaling up response_bytes for fast peers

How to Test

Run snap sync on Hoodi and compare total sync time against main:

cargo build --release --bin ethrex
./target/release/ethrex --network hoodi --authrpc.jwtsecret <jwt> --p2p.lookup-interval 10

Look for Sync cycle finished successfully time_elapsed_s=... in the logs. Run cargo test -p ethrex-p2p -- request_sizer and cargo test -p ethrex-trie -- trie_sorted for unit tests.

Adaptive request sizing: add per-peer response_bytes tuning based on
response latency. Start at 128KB, scale up 1.5x when latency < 2s,
scale down 1.5x when > 10s, clamped to [50KB, 2MB]. Applied to all
snap protocol request types (account ranges, storage ranges, trie
nodes, bytecodes). Includes RequestSizerMap in PeerHandler with 7
unit tests.

Storage range bisection: when a big-account sub-range returns
incomplete, bisect the remaining range into two sub-tasks dispatched
to different peers, doubling download bandwidth for large contracts.
Falls back to single continuation for ranges smaller than 2.
Partition the sorted account iterator into 16 buckets by first nibble
of H256 key and build each subtrie in parallel using std::thread::scope.
Merge the 16 subtrie root hashes into a single root BranchNode.

Refactor trie_from_sorted_accounts into inner + finalize layers so the
core loop can be reused for subtrie construction. Falls back to
sequential build when only 0-1 nibble buckets are non-empty.

Includes 5 new tests for parallel construction. Call site in
snap_sync.rs updated to use trie_from_sorted_accounts_parallel.
@ilitteri ilitteri requested a review from a team as a code owner February 11, 2026 16:40
@github-actions github-actions bot added L1 Ethereum client performance Block execution throughput and performance in general labels Feb 11, 2026
@github-actions
Copy link

🤖 Kimi Code Review

Review Summary

This PR introduces parallel trie building and adaptive request sizing for snap sync. Overall, the changes are well-structured and improve performance, but there are several issues to address:

Critical Issues

1. Race Condition in Parallel Trie Building (trie_sorted.rs:320-330)

The parallel trie building uses a shared TrieDB instance across threads without proper synchronization. The InMemoryTrieDB implementation appears to use Mutex<BTreeMap>, but other TrieDB implementations may not be thread-safe. This could lead to data races or corruption.

Fix: Add explicit synchronization or use thread-local DB instances.

2. Incorrect Subtrie Root Handling (trie_sorted.rs:408-415)

When merging subtrie results, the code incorrectly copies branch.choices[nibble] instead of using the entire branch's hash. This breaks the MPT structure.

// Current (incorrect):
root_choices[nibble] = branch.choices[nibble].clone();

// Should be:
root_choices[nibble] = branch.compute_hash(...); // or similar

3. Potential Overflow in Storage Range Bisection (client.rs:700-705)

The bisection logic uses U256 arithmetic without overflow checks. When range_size is 1, midpoint_u256 calculation could overflow.

Fix: Add overflow checks or use saturating arithmetic.

Security Issues

4. Unbounded Channel Growth (trie_sorted.rs:349-351)

The parallel trie builder creates bounded channels but doesn't handle the case where all buffers are exhausted, potentially causing deadlocks.

5. Request Sizer DoS Vector (request_sizer.rs:60-65)

The adaptive request sizer can be manipulated by malicious peers sending artificially delayed responses to reduce request sizes for legitimate peers.

Mitigation: Add peer reputation scoring or request size floors.

Performance Issues

6. Inefficient Partitioning (trie_sorted.rs:320-330)

The current partitioning only uses the first nibble (4 bits), which may create very uneven distributions. Consider using more bits or dynamic partitioning.

7. Excessive Thread Spawning (trie_sorted.rs:349-375)

Spawning 16 threads regardless of actual data distribution is wasteful. Only spawn threads for non-empty buckets.

Code Quality Issues

8. Type Complexity (trie_sorted.rs:163)

The #[allow(clippy::type_complexity)] should be removed by creating proper return types instead of using tuples.

9. Missing Error Handling (trie_sorted.rs:408)

Thread join errors are converted to TrieGenerationError::ThreadJoinError() but lose the original error context.

10. Inconsistent Naming (trie_sorted.rs:309 vs 349)

Function naming is inconsistent - trie_from_sorted_accounts_inner vs trie_from_sorted_accounts_parallel.

Minor Issues

  • Line 364: BUFFER_COUNT and SIZE_TO_WRITE_DB should be constants or configurable parameters
  • Line 408: The expect("Failed to spawn subtrie thread") should return a proper error
  • Line 700: U256::from_big_endian usage could be more efficient with direct byte access

Recommendations

  1. Add comprehensive tests for edge cases in parallel trie building
  2. Consider using a work-stealing thread pool instead of fixed partitioning
  3. Add metrics collection for adaptive request sizing effectiveness
  4. Document the thread-safety requirements for TrieDB implementations

The parallel trie building is a significant performance improvement, but the synchronization issues must be addressed before merging.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

🤖 Claude Code Review

Now I have a comprehensive view of the PR. Let me compile a detailed code review.

Code Review: PR #6181 - Adaptive Request Sizing, Storage Bisection, and Parallel Trie Construction

Summary

This PR implements three performance optimizations for snap sync: adaptive request sizing, storage range bisection, and parallel account trie construction. The implementation is well-structured with good test coverage. Below are specific findings organized by severity.


Critical Issues

1. Incorrect Subtrie Root Merging Logic (trie_sorted.rs:420)

root_choices[nibble] = branch.choices[nibble].clone();

Issue: This is extracting the wrong choice from the subtrie. Each subtrie's branch.choices[nibble] represents children at depth 2, but we need the root of that subtrie (which encompasses ALL its data under that first nibble).

Impact: This will produce incorrect state root hashes, breaking consensus. The parallel implementation will not match the sequential one.

Fix: The subtrie root should be computed and stored differently. The branch returned is the collapsed root-level branch of that subtrie, so we should be capturing the hash/reference to that entire branch, not just branch.choices[nibble].

Suggested approach:

  • Either return the hash of the subtrie root and construct a NodeHash choice
  • Or return the entire subtrie root structure to merge properly

High Priority Issues

2. Race Condition in Parallel Trie Construction (trie_sorted.rs:376)

let pool = Arc::new(ThreadPool::new(16, s));

Issue: All 16 subtrie threads share a single ThreadPool with 16 workers for DB writes. This could cause contention when multiple subtries try to flush nodes simultaneously.

Impact: Performance degradation, potential deadlock if all workers are occupied waiting for DB I/O.

Suggestion: Consider per-subtrie ThreadPools or a larger shared pool (e.g., 32-64 workers) to avoid blocking.


3. Missing Synchronization for Request Timing (snap/client.rs:468, 610, etc.)

request_sizer.record_response(&peer_id, request_start.elapsed());

Issue: record_response is only called on successful responses. If a request fails or times out, the peer's latency profile is not updated, potentially leaving slow/failing peers with inflated budgets.

Impact: Continued allocation of large requests to unresponsive peers, wasting bandwidth and time.

Fix: Record failures/timeouts with a penalty latency (e.g., Duration::from_secs(30)) to scale down their budgets.


4. Storage Bisection Interval Tracking Bug (snap/client.rs:728-735)

for (old_start, end) in old_intervals.iter_mut() {
    if *end == hash_end {
        *old_start = hash_start;
        *end = midpoint;
        break;
    }
}
old_intervals.push((midpoint_next, hash_end));

Issue: This modifies the first interval to end at midpoint, then adds a second interval starting at midpoint_next. However, if the original interval was [A, hash_end], it's now replaced with [hash_start, midpoint] and [midpoint_next, hash_end]. This is only correct if A == hash_start, which may not always be true - the original interval might have started at a different point.

Impact: Could lead to incorrect tracking of downloaded ranges, potential data corruption or incomplete downloads.

Verify: Ensure hash_start is indeed the current start of the interval being bisected, not just the start of the remaining sub-range.


5. Panic in Error Path (snap/client.rs:780)

if acc_hash.is_zero() {
    panic!("Should have found the account hash");
}

Issue: Using panic! in production code for an error condition that could theoretically occur due to data inconsistency or bugs.

Impact: Node crash during sync instead of graceful error handling.

Fix: Return a proper error: Err(SnapError::InternalError("Account hash not found in storage range processing".to_owned()))


Medium Priority Issues

6. Lock Poisoning Error Messages (snap/request_sizer.rs:75, 85)

.expect("RequestSizerMap lock poisoned")

Issue: Lock poisoning from a panic in one thread will crash all other threads trying to access the map.

Impact: Cascading failures during sync.

Suggestion: Use .unwrap_or_default() or return a default value, or propagate errors more gracefully. Since this is a performance optimization, reverting to INITIAL_RESPONSE_BYTES on lock failure is acceptable.


7. Integer Overflow in Bisection (snap/client.rs:707-710)

let midpoint_u256 = start_u256 + range_size / 2;
let midpoint = H256::from_uint(&midpoint_u256);
let midpoint_next_u256 = midpoint_u256 + 1;

Issue: If midpoint_u256 is U256::MAX, then midpoint_u256 + 1 wraps to 0. While unlikely with H256 hash ranges, it should be handled.

Impact: Incorrect range calculation leading to duplicate or missing storage slots.

Suggestion: Add overflow check or use saturating_add.


8. Hardcoded Thread Count (trie_sorted.rs:376)

let pool = Arc::new(ThreadPool::new(16, s));

Issue: Uses 16 threads for the pool regardless of available CPU cores. On machines with <16 cores, this causes oversubscription; on machines with >16 cores, it underutilizes resources.

Suggestion: Use std::thread::available_parallelism() or make it configurable.


9. Missing Error Handling in Parallel Trie (trie_sorted.rs:408)

.expect("Failed to spawn subtrie thread");

Issue: Thread spawn failures panic instead of returning an error. While rare, this could occur under resource exhaustion.

Suggestion: Return TrieGenerationError and fall back to sequential mode.


Low Priority / Style Issues

10. Unnecessary Clone (snap/client.rs:335)

buffer_sender.clone(),

Minor: The original buffer_sender is moved in the trie_from_sorted_accounts_inner call at line 331, so this clone at line 335 is necessary for the subsequent flush_nodes_to_write call. However, the code could be restructured to avoid the clone by using references more carefully, though this is a minor optimization.


11. Test Coverage Gap

Missing test: No test for the case where parallel trie construction produces a different result than sequential (i.e., the bug in issue #1). The existing tests compare parallel vs sequential, which is good, but they would all fail together if both are wrong.

Suggestion: Add a test with known state root from a real Ethereum block to validate correctness absolutely, not just consistency between implementations.


12. Magic Number Documentation (request_sizer.rs:18-21)

const LATENCY_LOW: Duration = Duration::from_secs(2);
const LATENCY_HIGH: Duration = Duration::from_secs(10);

Suggestion: Add comments explaining why these specific thresholds were chosen (e.g., based on Nethermind's implementation, empirical testing, or network characteristics).


Positive Observations

Excellent test coverage for RequestSizerMap - all edge cases covered
Good documentation - clear docstrings explaining the algorithms
Proper use of saturating_sub for safe arithmetic in bisection (line 704)
Correct MPT optimization - Single-choice branch collapse in finalize_root is properly implemented
Clean refactoring - Extracting _inner and finalize_root improves code reusability


Performance Considerations

13. Memory Usage in Parallel Trie Construction

The parallel implementation loads all accounts into memory (line 359-361) before processing, which doubles memory usage during account insertion phase. For chains with many accounts, this could be significant.

Tradeoff: This is acceptable for the 33% speedup, but worth monitoring in production.


14. Bisection Overhead

The bisection logic (lines 702-757) is complex and runs on the hot path. Ensure this doesn't add more overhead than it saves, especially for accounts with small storage.

Current mitigation: The range_size >= U256::from(2) check prevents degenerate cases. ✅


Security Considerations

15. No DoS Protection in Adaptive Sizing

A malicious peer could intentionally respond quickly to inflate its budget to 2MB, then become slow, wasting client bandwidth.

Mitigation: The scale-down mechanism (lines 49-52) will eventually throttle bad peers, but it may take several rounds. Consider a faster backoff or reputation system.

Verdict: Acceptable for current threat model, but document for future improvement.


Recommendations

  1. MUST FIX: Issue docs: add milestones #1 (incorrect subtrie merging) - this breaks correctness
  2. SHOULD FIX: Issues build: add Github actions boilerplate #3, docs: update milestones. #4, feat: add basic RPC api. #5 - these affect reliability and error handling
  3. CONSIDER: Issues chore: create project structure #2, Add lint and test jobs to CI #6, Add Dockerfile #7, build: add Dockerfile. #8 for robustness
  4. OPTIONAL: Style and documentation improvements

The optimization approach is sound and well-implemented overall, but the critical subtrie merging bug must be addressed before merge.


Automated review by Claude (Anthropic) · custom prompt

@greptile-apps
Copy link

greptile-apps bot commented Feb 11, 2026

Greptile Overview

Greptile Summary

Implemented three performance optimizations for snap sync: adaptive request sizing (1.5x scaling based on peer latency), storage range bisection for parallel big-account downloads, and 16-way parallel account trie construction.

Key Changes:

  • Added RequestSizerMap for per-peer adaptive response budgets (50KB-2MB range, 128KB initial)
  • Integrated latency tracking across all 5 snap protocol request types
  • Implemented storage range bisection to split incomplete responses into parallel sub-tasks
  • Refactored trie construction into _inner/finalize_root layers and added trie_from_sorted_accounts_parallel
  • Partitions accounts by first nibble (16 buckets) and builds subtries concurrently

Critical Issue:

  • Line 420 in trie_sorted.rs has incorrect branch choice selection for the parallel trie merge. Currently uses branch.choices[nibble] instead of computing the hash of the entire subtrie branch. This will produce incorrect state root hashes when accounts span multiple first nibbles, breaking state verification.

Testing:

  • 7 unit tests added for request sizer (scaling, clamping, per-peer independence)
  • 5 unit tests added for parallel trie construction (multi-nibble, single-nibble fallback, empty)
  • Benchmark results show 13% total speedup on Hoodi testnet

Confidence Score: 3/5

  • Critical logic bug in parallel trie construction will cause incorrect state root hashes
  • Score reflects a critical correctness bug in trie_sorted.rs:420 that will produce incorrect trie roots when the parallel construction path is taken. The adaptive sizing and bisection features are well-implemented, but the trie bug must be fixed before merging.
  • Pay close attention to crates/common/trie/trie_sorted.rs - the parallel trie root merge logic has a critical bug that must be fixed

Important Files Changed

Filename Overview
crates/networking/p2p/snap/request_sizer.rs New file implementing adaptive request sizing with per-peer latency tracking. Implementation is clean with comprehensive unit tests.
crates/common/trie/trie_sorted.rs Added parallel 16-way trie construction with refactored core logic. Critical bug found in line 420 where wrong branch choice is selected for root merge.
crates/networking/p2p/snap/client.rs Integrated adaptive request sizing and storage range bisection. Changes are well-structured with proper latency tracking.

Sequence Diagram

sequenceDiagram
    participant Client as Snap Client
    participant Sizer as RequestSizerMap
    participant Peer1 as Fast Peer
    participant Peer2 as Slow Peer
    participant DB as Trie DB
    
    Note over Client,DB: Adaptive Request Sizing Flow
    Client->>Sizer: response_bytes_for_peer(peer1)
    Sizer-->>Client: 128KB (initial)
    Client->>Peer1: GetAccountRange(128KB)
    Note over Peer1: Response in 1s
    Peer1-->>Client: AccountRange data
    Client->>Sizer: record_response(peer1, 1s)
    Note over Sizer: Latency < 2s → scale up 1.5x
    
    Client->>Sizer: response_bytes_for_peer(peer1)
    Sizer-->>Client: 192KB (scaled up)
    
    Client->>Sizer: response_bytes_for_peer(peer2)
    Sizer-->>Client: 128KB (initial)
    Client->>Peer2: GetStorageRanges(128KB)
    Note over Peer2: Response in 12s
    Peer2-->>Client: StorageRanges data
    Client->>Sizer: record_response(peer2, 12s)
    Note over Sizer: Latency > 10s → scale down 1.5x
    
    Note over Client,DB: Storage Range Bisection
    Client->>Peer1: GetStorageRanges[hash_start..hash_end]
    Peer1-->>Client: Incomplete response (partial)
    Note over Client: Bisect range at midpoint
    Client->>Peer1: GetStorageRanges[hash_start..midpoint]
    Client->>Peer2: GetStorageRanges[midpoint+1..hash_end]
    Note over Client: Parallel download from 2 peers
    
    Note over Client,DB: Parallel Trie Construction
    par 16-way parallel build
        Client->>DB: Subtrie 0 (nibble 0x0)
        Client->>DB: Subtrie 1 (nibble 0x1)
        Client->>DB: Subtrie F (nibble 0xF)
    end
    Note over Client,DB: Merge 16 subtrie roots
    Client->>DB: Write root BranchNode
Loading

@github-actions
Copy link

🤖 Codex Code Review

Here are the findings from the PR diff.

Findings

  • High: trie_from_sorted_accounts_parallel fully materializes the entire iterator into 16 in‑memory buckets, which removes the streaming/memory‑bounded behavior of trie_from_sorted_accounts_wrap and can blow up memory for full state sync. This is especially risky now that snap_sync::insert_accounts switched to the parallel builder. Consider a streaming partitioner (e.g., prefix iter per nibble) or a disk‑backed spill strategy. crates/common/trie/trie_sorted.rs:357-372, crates/networking/p2p/sync/snap_sync.rs:940-967
  • Medium: Root merge assumes each subtrie root has a single choice at the bucket nibble and silently drops any other choices by indexing branch.choices[nibble]. If input ordering or bucketing ever deviates (e.g., due to endianness mismatch or unsorted data), this produces an incorrect root without detection. Consider an explicit check/assert that the subtrie root has exactly one valid choice and that its index matches the bucket nibble. crates/common/trie/trie_sorted.rs:413-421
  • Low: RequestSizerMap panics on poisoned mutex, which can crash the node due to a prior panic in any task touching the map. In networking/sync paths, prefer recovering by resetting the entry or map instead of panicking. crates/networking/p2p/snap/request_sizer.rs:62-93

If you want, I can dig further into the trie build logic and verify the branch merge against the MPT invariants, or prototype a memory‑safe parallelization approach.

Potential next steps:

  1. Add a memory/regression test for the parallel trie builder on large iterators.
  2. Add invariant checks on subtrie roots before merging.
  3. Switch RequestSizerMap to a non‑poisoning strategy (e.g., lock().unwrap_or_else(|e| e.into_inner())).

Automated review by OpenAI Codex · custom prompt

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

.join()
.map_err(|_| TrieGenerationError::ThreadJoinError())??;
if let Some(branch) = branch_opt {
root_choices[nibble] = branch.choices[nibble].clone();
Copy link

Choose a reason for hiding this comment

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

incorrect branch choice selection

Each subtrie builds nodes for keys starting with nibble N, so the root choice at index N should reference the subtrie's root. However, this line uses branch.choices[nibble] which reads the Nth choice from the subtrie's root branch, not the subtrie's own root hash.

Suggested change
root_choices[nibble] = branch.choices[nibble].clone();
root_choices[nibble] = branch.compute_hash_no_alloc(&mut Vec::new()).into();

This will cause incorrect trie root hashes when accounts span multiple nibbles.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/trie/trie_sorted.rs
Line: 420:420

Comment:
incorrect branch choice selection

Each subtrie builds nodes for keys starting with nibble N, so the root choice at index N should reference the subtrie's root. However, this line uses `branch.choices[nibble]` which reads the Nth choice from the subtrie's root branch, not the subtrie's own root hash.

```suggestion
                root_choices[nibble] = branch.compute_hash_no_alloc(&mut Vec::new()).into();
```

This will cause incorrect trie root hashes when accounts span multiple nibbles.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link

Lines of code report

Total lines added: 369
Total lines removed: 0
Total lines changed: 369

Detailed view
+------------------------------------------------------+-------+------+
| File                                                 | Lines | Diff |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/trie_sorted.rs             | 596   | +166 |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/peer_handler.rs         | 549   | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/snap/client.rs          | 1244  | +62  |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/snap/constants.rs       | 26    | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/snap/mod.rs             | 24    | +2   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/snap/request_sizer.rs   | 128   | +128 |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/healing/state.rs   | 415   | +2   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/healing/storage.rs | 617   | +3   |
+------------------------------------------------------+-------+------+

Copy link
Contributor

@ElFantasma ElFantasma left a comment

Choose a reason for hiding this comment

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

4 comments, but mostly observations, take a look to be sure you are ok with those or you prefer some fix. I'm approving this in any case since it LGTM

.join()
.map_err(|_| TrieGenerationError::ThreadJoinError())??;
if let Some(branch) = branch_opt {
root_choices[nibble] = branch.choices[nibble].clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

This line relies on the invariant that trie_from_sorted_accounts_inner, when fed keys all sharing the same first nibble N, returns a BranchNode where only choices[N] is populated. That's true today because _inner processes full H256 paths (not stripped keys), but it's subtle and fragile — a future caller passing pre-stripped keys would silently produce a wrong trie.

Consider adding a debug assertion:

if let Some(branch) = branch_opt {
    debug_assert!(
        branch.choices.iter().enumerate()
            .filter(|(_, c)| c.is_valid())
            .all(|(i, _)| i == nibble),
        "subtrie for nibble {nibble} has unexpected populated choices"
    );
    root_choices[nibble] = branch.choices[nibble].clone();
}


// Build 16 subtries in parallel using scoped threads + shared ThreadPool for DB writes
scope(|s| {
let pool = Arc::new(ThreadPool::new(16, s));
Copy link
Contributor

Choose a reason for hiding this comment

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

This creates a ThreadPool with 16 threads for DB writes, plus 16 scoped threads (one per nibble) are spawned at line 388. That's up to 32 threads competing for CPU. On the benchmark machine (12 cores) this is fine, but on smaller machines the over-subscription could hurt.

Consider sizing the pool based on available parallelism, e.g.:

let num_threads = std::thread::available_parallelism()
    .map(|n| n.get())
    .unwrap_or(4);
let pool = Arc::new(ThreadPool::new(num_threads, s));

/// Cheaply cloneable (Arc-backed). Pass clones to spawned tasks.
#[derive(Debug, Clone, Default)]
pub struct RequestSizerMap {
inner: Arc<Mutex<HashMap<H256, PeerSizer>>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Entries for disconnected peers are never removed from this HashMap, so it grows monotonically over the lifetime of the sync. Each entry is small (~16 bytes), but during a long mainnet sync the node may see hundreds of unique peers.

Not critical, but worth noting — could add a simple cap (e.g., evict oldest entries when size exceeds 256) or tie cleanup to peer disconnection events.

pub const MIN_RESPONSE_BYTES_ADAPTIVE: u64 = 50 * 1024;

/// Maximum response bytes for adaptive sizing (2 MB).
pub const MAX_RESPONSE_BYTES_ADAPTIVE: u64 = 2 * 1024 * 1024;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: MAX_RESPONSE_BYTES_ADAPTIVE (2 MB) exceeds the server-side cap MAX_RESPONSE_BYTES (512 KB). Against ethrex peers, requests above 512 KB will never return more data — the peer truncates at its own limit. This means the sizer will plateau at 512 KB effective throughput regardless of how high it scales.

Not a bug (peers just return less), but might be worth documenting the relationship, or capping at MAX_RESPONSE_BYTES unless explicitly targeting non-ethrex peers.

@github-project-automation github-project-automation bot moved this to In Review in ethrex_l1 Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client performance Block execution throughput and performance in general

Projects

Status: In Review
Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants