foundry_evm_core/fork/
prefetch.rs1use super::Fork;
11use alloy_network::Network;
12use alloy_primitives::{Address, U256};
13use foundry_config::Config;
14use foundry_fork_db::{
15 ForkBlockEnv, SharedBackend,
16 backend::BlockingMode,
17 cache::{JsonBlockCacheDB, MemDb},
18};
19use parking_lot::Mutex;
20use std::{
21 path::{Path, PathBuf},
22 sync::{
23 Arc,
24 atomic::{AtomicBool, Ordering},
25 mpsc,
26 },
27};
28
29const MAX_CONCURRENT_REQUESTS: usize = 32;
32
33const MAX_KEYS: usize = 4096;
36
37enum Key {
38 Account(Address),
39 Slot(Address, U256),
40}
41
42pub(crate) fn spawn<N: Network, B: ForkBlockEnv>(
48 fork: &Fork,
49 backend: &SharedBackend<N, B>,
50) -> Option<Prefetch> {
51 if !backend.data().accounts.read().is_empty() {
52 return None;
53 }
54 let runtime = tokio::runtime::Handle::try_current().ok()?;
55 let (chain, number) = (fork.context().source_chain_id, fork.number());
56 let stop = Arc::new(AtomicBool::new(false));
57 let (done_tx, done) = mpsc::channel::<()>();
58 let backend = backend.with_blocking_mode(BlockingMode::Block);
59 let coordinator_stop = Arc::clone(&stop);
60 runtime.clone().spawn_blocking(move || {
61 let stop = coordinator_stop;
62 if let Some(path) =
63 Config::foundry_chain_cache_dir(chain).and_then(|dir| previous_cache_file(&dir, number))
64 && let Ok(previous) = JsonBlockCacheDB::<B>::load(&path)
65 && !stop.load(Ordering::Relaxed)
66 {
67 let keys = keys(previous.db());
68 debug!(target: "fork::prefetch", ?path, keys = keys.len(), "prefetching fork cache");
69 let keys = Arc::new(Mutex::new(keys.into_iter()));
71 for _ in 0..MAX_CONCURRENT_REQUESTS {
72 let (keys, stop, done_tx) = (Arc::clone(&keys), Arc::clone(&stop), done_tx.clone());
73 let backend = backend.clone();
74 runtime.spawn_blocking(move || {
75 while !stop.load(Ordering::Relaxed) {
76 let Some(key) = keys.lock().next() else { break };
77 let read = match key {
79 Key::Account(address) => backend.prefetch_basic(address),
80 Key::Slot(address, slot) => backend.prefetch_storage(address, slot),
81 };
82 if read.is_err() {
84 break;
85 }
86 }
87 drop(backend);
89 drop(done_tx);
90 });
91 }
92 }
93 drop(backend);
95 drop(done_tx);
96 });
97 Some(Prefetch { stop, done })
98}
99
100pub(crate) struct Prefetch {
102 stop: Arc<AtomicBool>,
103 done: mpsc::Receiver<()>,
104}
105
106impl Prefetch {
107 pub(crate) fn stop(self) {
110 self.stop.store(true, Ordering::Relaxed);
111 let _ = self.done.recv();
113 }
114}
115
116fn previous_cache_file(dir: &Path, number: u64) -> Option<PathBuf> {
118 std::fs::read_dir(dir)
119 .ok()?
120 .filter_map(|entry| {
121 let entry = entry.ok()?;
122 let block = entry.file_name().to_str()?.parse::<u64>().ok()?;
123 (block < number && entry.file_type().ok()?.is_file()).then_some((block, entry.path()))
124 })
125 .max_by_key(|(block, _)| *block)
126 .map(|(_, path)| path)
127}
128
129fn keys(db: &MemDb) -> Vec<Key> {
131 let accounts = db.accounts.read();
132 let storage = db.storage.read();
133 accounts
134 .keys()
135 .map(|&address| Key::Account(address))
136 .chain(
137 storage.iter().flat_map(|(&address, slots)| {
138 slots.keys().map(move |&slot| Key::Slot(address, slot))
139 }),
140 )
141 .take(MAX_KEYS)
142 .collect()
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn picks_newest_cache_file_below_block() {
151 let dir = std::env::temp_dir().join(format!("foundry-prefetch-{}", std::process::id()));
152 std::fs::create_dir_all(dir.join("120")).unwrap();
153 for block in ["90", "100", "110", "130", "notes"] {
154 std::fs::write(dir.join(block), b"{}").unwrap();
155 }
156
157 assert_eq!(previous_cache_file(&dir, 125), Some(dir.join("110")));
159 assert_eq!(previous_cache_file(&dir, 90), None);
160 std::fs::remove_dir_all(&dir).unwrap();
161 }
162}