Skip to content

perf(fork): add fork_prefetch to warm new fork blocks - #17673

Merged
figtracer merged 5 commits into
masterfrom
fig/fork-prefetch
Oct 21, 2026
Merged

figtracer merged 5 commits into
masterfrom
fig/fork-prefetch

Conversation

@figtracer

@figtracer figtracer commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Why

Before: a fork of latest starts with an empty cache on each new head, so every cold account or storage read blocks the EVM for one RPC round trip. CI suites that fork latest pay this on every run.

After: with fork_prefetch = true, a new fork block fetches the keys that earlier runs read, in parallel and in the background, so most reads hit the cache.

Change

When a fork starts on a block with an empty disk cache, it takes the keys of the newest older cache file on the same chain and reads them through the fork backend on 32 background workers. The backend fetches and caches each key like an EVM read, so results do not change, and an unused key costs one request. The fork_prefetch setting, the --fork-prefetch flag, and FOUNDRY_FORK_PREFETCH turn it on; it is off by default and follows the disk caching settings.

Results

Each run forks a new head of ethereum.reth.rs, and the modes alternate in one release binary.

Suite Baseline Prefetch Saving per run
sky-ecosystem/dss-allocator, 108 tests, 6 pairs 20.33 s 9.74 s 10.59 s ± 0.22 s
Uniswap/permit2 integration, 8 of 10 pairs 5.18 s 2.35 s 2.83 s ± 0.09 s

On dss-allocator, cold RPCs from the EVM drop from 534 storage and 57 account reads to 5–77 and 4–7, for 592 prefetched keys. Two permit2 pairs are left out because of slow TCP connects to the endpoint, which happened in both modes.

Downstream

This adds a fork_prefetch config key (default false) to forge config output and a --fork-prefetch flag to the shared EVM arguments. Default behavior does not change. The book update is foundry-rs/book#2179.

A fork of `latest` starts with an empty cache on each new head, so every
cold account or storage read blocks the EVM for one RPC round trip. With
`fork_prefetch = true` (or `--fork-prefetch`), a fork that starts on a
block with an empty disk cache reads the keys of the newest older cache
file of the same chain, fetches them at the new block with 32 requests
in flight, and inserts them only where the cache has no value. Values
come from the pinned block, so results do not change; an unused key
only costs a request. The setting defaults to off and follows the disk
caching settings.

Measured on ethereum.reth.rs with each run on a new head, alternating
modes in one binary:
- sky-ecosystem/dss-allocator (108 tests, all suites fork latest),
  6 pairs: 20.33 s -> 9.74 s per run (-10.59 s +- 0.22 s). EVM cold
  RPCs 534 storage + 57 account -> 5-77 + 4-7, with 592 keys prefetched.
- Uniswap/permit2 integration, 10 pairs: 5.18 s -> 2.35 s per run
  (-2.83 s +- 0.09 s over the 8 pairs without an endpoint stall).
Read the predicted keys through the fork's SharedBackend on blocking
workers instead of a separate fetcher. The prefetch now uses the
backend's own request path, deduplication and cache writes, so it no
longer copies the account fetch policy, request parameters or code hash
conversion.
@figtracer
figtracer marked this pull request as ready for review October 19, 2026 20:04
@figtracer figtracer changed the title perf(fork): add fork_prefetch to warm new fork blocks perf(fork): add fork_prefetch to warm new fork blocks Oct 19, 2026
mattsse
mattsse previously approved these changes Oct 19, 2026

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

Comment thread crates/evm/core/src/fork/prefetch.rs Outdated
A failed speculative read is not an error, but the backend logs one per
failure, and every remaining read fails once the run ends. Run the
prefetch workers without a tracing subscriber and stop each one at its
first failed read.
Comment thread crates/evm/core/src/fork/prefetch.rs Outdated
Comment thread crates/evm/core/src/fork/multi.rs Outdated
The prefetch workers hold backend clones, and the last clone flushes
the disk cache. Keep the worker handles in the fork handler, and on
shutdown stop them and wait after the backend handlers are dropped, so
the flush finishes before the shutdown completes.

Start prefetch only for forks that follow `latest`. Rolls and forks
pinned to a block do not start workers.
@figtracer
figtracer requested a review from mablr October 21, 2026 13:20
Comment thread crates/evm/core/src/fork/prefetch.rs Outdated
Comment thread crates/evm/core/src/fork/prefetch.rs
Finding and decoding the previous cache file ran inside the fork
handler's poll, so it stalled every other backend's reads and the fork
creation. A background coordinator now loads the keys and starts the
workers. It holds its own backend clone and done sender, so shutdown
still waits for it before the cache flush completes.
@figtracer
figtracer requested a review from mablr October 21, 2026 15:05

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@figtracer
figtracer merged commit 2ad406e into master Oct 21, 2026
24 checks passed
@figtracer
figtracer deleted the fig/fork-prefetch branch October 21, 2026 20:06
@github-project-automation github-project-automation Bot moved this to Done in Foundry Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants