Skip to content

perf(engine): merge pruning into save_blocks write transaction#21886

Closed
yongkangc wants to merge 4 commits intomainfrom
merge-prune-into-save-tx
Closed

perf(engine): merge pruning into save_blocks write transaction#21886
yongkangc wants to merge 4 commits intomainfrom
merge-prune-into-save-tx

Conversation

@yongkangc
Copy link
Member

Summary
After on_save_blocks committed blocks (fsync #1), the persistence thread ran pruning in a separate MDBX write transaction with its own commit (fsync #2). During this entire pruning pass, the persistence thread could not process new requests. This merges pruning into the same write transaction by calling Pruner::run_with_provider() with the existing provider_rw before commit(), eliminating the second fsync entirely.

Changes

  • Move pruning into on_save_blocks before provider_rw.commit() using the existing run_with_provider API
  • Catch and log prune errors without preventing block persistence (preserves existing guarantee)
  • Remove standalone prune_before method and post-save pruning from run() loop
  • Clean up unused PrunerOutput import

Expected Impact
Eliminates ~128ms of redundant fsync latency on every prune cycle (fires every other save). Based on bench metrics: prune accounts for 14.9% of total persistence wall time (53s / 356s). Each persistence cycle becomes a single atomic DB update with one fsync instead of two.

Notes
The PersistenceError::PrunerError variant is retained for API compatibility even though it is no longer constructed in this code path. The pruner's built-in delete_limit and timeout bound the additional transaction duration.

Previously, `PrewarmCacheTask::save_cache` called `valid_block_rx.recv()`
inside `PayloadExecutionCache::update_with_guard`, which holds a write lock.
This blocking call could stall all readers/writers of the execution cache
indefinitely, causing system-wide contention (observed via 5ms lock
contention warnings in `get_cache_for()`).

This change:
1. Moves all cache building operations (clone, insert_state, update_metrics)
   outside the write lock
2. Moves the blocking `recv()` call outside the lock
3. Adds conditional swap with parent hash check to prevent race conditions
4. Keeps the usage guard alive until after recv() to prevent another thread
   from clearing the cache we're still using

The lock is now only held for the minimal final assignment.

Amp-Thread-ID: https://ampcode.com/threads/T-019c2db8-0945-765a-979e-99d922bd4791
Previously, after on_save_blocks committed blocks (fsync #1), the
persistence thread ran pruning in a separate MDBX write transaction
with its own commit (fsync #2). During this entire pruning pass, the
persistence thread could not process new requests.

Merge pruning into the same write transaction as save_blocks by calling
Pruner::run_with_provider() with the existing provider_rw before commit.
This eliminates the second fsync entirely — one write transaction, one
commit, one fsync per cycle.

Prune errors are caught and logged but do not prevent block persistence.
This preserves the existing guarantee that blocks are always committed
regardless of prune outcome.

Based on bench metrics (rf7d8): save p50=305ms, prune p50=128ms firing
every other save. Prune accounts for 14.9% of total persistence wall
time (53s / 356s). This change eliminates ~128ms of redundant fsync
latency on every prune cycle.

Amp-Thread-ID: https://ampcode.com/threads/T-019c3183-3b50-7379-8a4b-42f7a68aac22
@yongkangc yongkangc added the A-engine Related to the engine implementation label Feb 6, 2026
@github-project-automation github-project-automation bot moved this to Backlog in Reth Tracker Feb 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

⚠️ Changelog not found.

A changelog entry is required before merging. We've generated a suggested changelog based on your changes:

Preview
---
reth-engine-tree: patch
---

Fixed account cache size metrics and merged pruning into save_blocks write transaction. Resolved clippy collapsible-if by using if-let for outer guard, added debug log for race condition skip in cache updates, and restored decrement_size call when removing accounts from cache.

Add changelog to commit this to your branch.

Copy link
Member

@Rjected Rjected left a comment

Choose a reason for hiding this comment

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

I do think it would be worth adding helpers for running the pruner with the same provider_rw, so that we only commit once per batch of blocks, would like the cached state / unrelated changes removed though.

Comment on lines -77 to -88

/// Prunes block data before the given block number according to the configured prune
/// configuration.
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(block_num))]
fn prune_before(&mut self, block_num: u64) -> Result<PrunerOutput, PrunerError> {
debug!(target: "engine::persistence", ?block_num, "Running pruner");
let start_time = Instant::now();
// TODO: doing this properly depends on pruner segment changes
let result = self.pruner.run(block_num);
self.metrics.prune_before_duration_seconds.record(start_time.elapsed());
result
}
Copy link
Member

Choose a reason for hiding this comment

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

would like to keep a helper method for this if possible and the instrumentation

@github-project-automation github-project-automation bot moved this from Backlog to In Progress in Reth Tracker Feb 6, 2026
@yongkangc yongkangc closed this Feb 11, 2026
@github-project-automation github-project-automation bot moved this from In Progress to Done in Reth Tracker Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-engine Related to the engine implementation

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants