Skip to content

Commit ced313e

Browse files
authored
perf: compute literals only once (#12716)
* perf: compute literals only once * init on setup * fix: deadlock * fix: Default for FuzzDictionary * chore: clippy * fix: defaults * fix: usize::min to allow disabling with 0. still unintuitive * chore: fast path max > 0 * chore: break if break * chore: consider only fuzz dictionary config
1 parent 46264be commit ced313e

File tree

8 files changed

+130
-105
lines changed

8 files changed

+130
-105
lines changed

crates/evm/evm/src/executors/fuzz/mod.rs

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,19 @@ impl FuzzedExecutor {
9999
&mut self,
100100
func: &Function,
101101
fuzz_fixtures: &FuzzFixtures,
102-
deployed_libs: &[Address],
102+
state: EvmFuzzState,
103103
address: Address,
104104
rd: &RevertDecoder,
105105
progress: Option<&ProgressBar>,
106106
early_exit: &EarlyExit,
107107
) -> Result<FuzzTestResult> {
108+
let state = &state;
108109
// Stores the fuzz test execution data.
109110
let mut test_data = FuzzTestData::default();
110-
let state = self.build_fuzz_state(deployed_libs);
111111
let dictionary_weight = self.config.dictionary.dictionary_weight.min(100);
112112
let strategy = proptest::prop_oneof![
113113
100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
114-
dictionary_weight => fuzz_calldata_from_state(func.clone(), &state),
114+
dictionary_weight => fuzz_calldata_from_state(func.clone(), state),
115115
]
116116
.prop_map(move |calldata| BasicTxDetails {
117117
warp: None,
@@ -173,7 +173,7 @@ impl FuzzedExecutor {
173173

174174
test_data.runs += 1;
175175

176-
match corpus_manager.new_input(&mut self.runner, &state, func) {
176+
match corpus_manager.new_input(&mut self.runner, state, func) {
177177
Ok(input) => input,
178178
Err(err) => {
179179
test_data.failure = Some(TestCaseError::fail(format!(
@@ -382,27 +382,4 @@ impl FuzzedExecutor {
382382
}))
383383
}
384384
}
385-
386-
/// Stores fuzz state for use with [fuzz_calldata_from_state]
387-
pub fn build_fuzz_state(&self, deployed_libs: &[Address]) -> EvmFuzzState {
388-
let inspector = self.executor.inspector();
389-
390-
if let Some(fork_db) = self.executor.backend().active_fork_db() {
391-
EvmFuzzState::new(
392-
fork_db,
393-
self.config.dictionary,
394-
deployed_libs,
395-
inspector.analysis.as_ref(),
396-
inspector.paths_config(),
397-
)
398-
} else {
399-
EvmFuzzState::new(
400-
self.executor.backend().mem_db(),
401-
self.config.dictionary,
402-
deployed_libs,
403-
inspector.analysis.as_ref(),
404-
inspector.paths_config(),
405-
)
406-
}
407-
}
408385
}

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ impl<'a> InvariantExecutor<'a> {
331331
&mut self,
332332
invariant_contract: InvariantContract<'_>,
333333
fuzz_fixtures: &FuzzFixtures,
334-
deployed_libs: &[Address],
334+
fuzz_state: EvmFuzzState,
335335
progress: Option<&ProgressBar>,
336336
early_exit: &EarlyExit,
337337
) -> Result<InvariantFuzzTestResult> {
@@ -341,7 +341,7 @@ impl<'a> InvariantExecutor<'a> {
341341
}
342342

343343
let (mut invariant_test, mut corpus_manager) =
344-
self.prepare_test(&invariant_contract, fuzz_fixtures, deployed_libs)?;
344+
self.prepare_test(&invariant_contract, fuzz_fixtures, fuzz_state)?;
345345

346346
// Start timer for this invariant test.
347347
let mut runs = 0;
@@ -552,23 +552,13 @@ impl<'a> InvariantExecutor<'a> {
552552
&mut self,
553553
invariant_contract: &InvariantContract<'_>,
554554
fuzz_fixtures: &FuzzFixtures,
555-
deployed_libs: &[Address],
555+
fuzz_state: EvmFuzzState,
556556
) -> Result<(InvariantTest, CorpusManager)> {
557557
// Finds out the chosen deployed contracts and/or senders.
558558
self.select_contract_artifacts(invariant_contract.address)?;
559559
let (targeted_senders, targeted_contracts) =
560560
self.select_contracts_and_senders(invariant_contract.address)?;
561561

562-
// Stores fuzz state for use with [fuzz_calldata_from_state].
563-
let inspector = self.executor.inspector();
564-
let fuzz_state = EvmFuzzState::new(
565-
self.executor.backend().mem_db(),
566-
self.config.dictionary,
567-
deployed_libs,
568-
inspector.analysis.as_ref(),
569-
inspector.paths_config(),
570-
);
571-
572562
// Creates the invariant strategy.
573563
let strategy = invariant_strat(
574564
fuzz_state.clone(),

crates/evm/fuzz/src/strategies/literals.rs

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ use solar::{
99
ast::{self, Visit},
1010
interface::source_map::FileName,
1111
};
12-
use std::{ops::ControlFlow, sync::OnceLock};
12+
use std::{
13+
ops::ControlFlow,
14+
sync::{Arc, OnceLock},
15+
};
1316

14-
#[derive(Debug, Default)]
17+
#[derive(Clone, Debug)]
1518
pub struct LiteralsDictionary {
16-
/// Data required for initialization, captured from `EvmFuzzState::new`.
17-
analysis: Option<Analysis>,
18-
paths_config: Option<ProjectPathsConfig>,
19-
max_values: usize,
19+
maps: Arc<OnceLock<LiteralMaps>>,
20+
}
2021

21-
/// Lazy initialized literal maps.
22-
maps: OnceLock<LiteralMaps>,
22+
impl Default for LiteralsDictionary {
23+
fn default() -> Self {
24+
Self::new(None, None, usize::MAX)
25+
}
2326
}
2427

2528
impl LiteralsDictionary {
@@ -28,42 +31,42 @@ impl LiteralsDictionary {
2831
paths_config: Option<ProjectPathsConfig>,
2932
max_values: usize,
3033
) -> Self {
31-
Self { analysis, paths_config, max_values, maps: OnceLock::default() }
34+
let maps = Arc::new(OnceLock::<LiteralMaps>::new());
35+
if let Some(analysis) = analysis
36+
&& max_values > 0
37+
{
38+
let maps = maps.clone();
39+
// This can't be done in a rayon task (including inside of `get`) because it can cause a
40+
// deadlock, since internally `solar` also uses rayon.
41+
let _ = std::thread::Builder::new().name("literal-collector".into()).spawn(move || {
42+
let _ = maps.get_or_init(|| {
43+
let literals =
44+
LiteralsCollector::process(&analysis, paths_config.as_ref(), max_values);
45+
debug!(
46+
words = literals.words.values().map(|set| set.len()).sum::<usize>(),
47+
strings = literals.strings.len(),
48+
bytes = literals.bytes.len(),
49+
"collected source code literals for fuzz dictionary"
50+
);
51+
literals
52+
});
53+
});
54+
} else {
55+
maps.set(Default::default()).unwrap();
56+
}
57+
Self { maps }
3258
}
3359

34-
/// Returns a reference to the `LiteralMaps`, initializing them on the first call.
60+
/// Returns a reference to the `LiteralMaps`.
3561
pub fn get(&self) -> &LiteralMaps {
36-
self.maps.get_or_init(|| {
37-
if let Some(analysis) = &self.analysis {
38-
let literals = LiteralsCollector::process(
39-
analysis,
40-
self.paths_config.as_ref(),
41-
self.max_values,
42-
);
43-
trace!(
44-
words = literals.words.values().map(|set| set.len()).sum::<usize>(),
45-
strings = literals.strings.len(),
46-
bytes = literals.bytes.len(),
47-
"collected source code literals for fuzz dictionary"
48-
);
49-
literals
50-
} else {
51-
LiteralMaps::default()
52-
}
53-
})
54-
}
55-
56-
/// Takes ownership of the dictionary words, leaving an empty map in their place.
57-
/// Ensures the map is initialized before taking its contents.
58-
pub fn take_words(&mut self) -> HashMap<DynSolType, B256IndexSet> {
59-
let _ = self.get();
60-
self.maps.get_mut().map(|m| std::mem::take(&mut m.words)).unwrap_or_default()
62+
self.maps.wait()
6163
}
6264

63-
#[cfg(test)]
6465
/// Test-only helper to seed the dictionary with literal values.
66+
#[cfg(test)]
6567
pub(crate) fn set(&mut self, map: super::LiteralMaps) {
66-
let _ = self.maps.set(map);
68+
self.maps = Arc::new(OnceLock::new());
69+
self.maps.set(map).unwrap();
6770
}
6871
}
6972

@@ -102,8 +105,10 @@ impl LiteralsCollector {
102105
continue;
103106
}
104107

105-
if let Some(ref ast) = source.ast {
106-
let _ = literals_collector.visit_source_unit(ast);
108+
if let Some(ast) = &source.ast
109+
&& literals_collector.visit_source_unit(ast).is_break()
110+
{
111+
break;
107112
}
108113
}
109114

crates/evm/fuzz/src/strategies/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ mod mutators;
2020
pub use mutators::BoundMutator;
2121

2222
mod literals;
23-
pub use literals::{LiteralMaps, LiteralsCollector};
23+
pub use literals::{LiteralMaps, LiteralsCollector, LiteralsDictionary};

crates/evm/fuzz/src/strategies/param.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,16 +433,13 @@ mod tests {
433433
};
434434
use alloy_primitives::B256;
435435
use foundry_common::abi::get_func;
436-
use foundry_config::FuzzDictionaryConfig;
437-
use revm::database::{CacheDB, EmptyDB};
438436
use std::collections::HashSet;
439437

440438
#[test]
441439
fn can_fuzz_array() {
442440
let f = "testArray(uint64[2] calldata values)";
443441
let func = get_func(f).unwrap();
444-
let db = CacheDB::new(EmptyDB::default());
445-
let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
442+
let state = EvmFuzzState::test();
446443
let strategy = proptest::prop_oneof![
447444
60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
448445
40 => fuzz_calldata_from_state(func, &state),
@@ -467,8 +464,7 @@ mod tests {
467464
literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
468465
literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
469466

470-
let db = CacheDB::new(EmptyDB::default());
471-
let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None);
467+
let state = EvmFuzzState::test();
472468
state.seed_literals(literals);
473469

474470
let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };

crates/evm/fuzz/src/strategies/state.rs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use alloy_primitives::{
88
map::{AddressIndexSet, AddressMap, B256IndexSet, HashMap, IndexSet},
99
};
1010
use foundry_common::{
11-
compile::Analysis, ignore_metadata_hash, mapping_slots::MappingSlots,
12-
slot_identifier::SlotIdentifier,
11+
ignore_metadata_hash, mapping_slots::MappingSlots, slot_identifier::SlotIdentifier,
1312
};
14-
use foundry_compilers::{ProjectPathsConfig, artifacts::StorageLayout};
13+
use foundry_compilers::artifacts::StorageLayout;
1514
use foundry_config::FuzzDictionaryConfig;
1615
use foundry_evm_core::{bytecode::InstIter, utils::StateChangeset};
1716
use parking_lot::{RawRwLock, RwLock, lock_api::RwLockReadGuard};
@@ -43,12 +42,21 @@ pub struct EvmFuzzState {
4342
}
4443

4544
impl EvmFuzzState {
45+
#[cfg(test)]
46+
pub(crate) fn test() -> Self {
47+
Self::new(
48+
&[],
49+
&CacheDB::<revm::database::EmptyDB>::default(),
50+
FuzzDictionaryConfig::default(),
51+
None,
52+
)
53+
}
54+
4655
pub fn new<DB: DatabaseRef>(
56+
deployed_libs: &[Address],
4757
db: &CacheDB<DB>,
4858
config: FuzzDictionaryConfig,
49-
deployed_libs: &[Address],
50-
analysis: Option<&Analysis>,
51-
paths_config: Option<&ProjectPathsConfig>,
59+
literals: Option<&LiteralsDictionary>,
5260
) -> Self {
5361
// Sort accounts to ensure deterministic dictionary generation from the same setUp state.
5462
let mut accs = db.cache.accounts.iter().collect::<Vec<_>>();
@@ -57,11 +65,9 @@ impl EvmFuzzState {
5765
// Create fuzz dictionary and insert values from db state.
5866
let mut dictionary = FuzzDictionary::new(config);
5967
dictionary.insert_db_values(accs);
60-
dictionary.literal_values = LiteralsDictionary::new(
61-
analysis.cloned(),
62-
paths_config.cloned(),
63-
config.max_fuzz_dictionary_literals,
64-
);
68+
if let Some(literals) = literals {
69+
dictionary.literal_values = literals.clone();
70+
}
6571

6672
Self {
6773
inner: Arc::new(RwLock::new(dictionary)),
@@ -126,16 +132,15 @@ impl EvmFuzzState {
126132
self.inner.read().log_stats();
127133
}
128134

129-
#[cfg(test)]
130135
/// Test-only helper to seed the dictionary with literal values.
136+
#[cfg(test)]
131137
pub(crate) fn seed_literals(&self, map: super::LiteralMaps) {
132138
self.inner.write().seed_literals(map);
133139
}
134140
}
135141

136142
// We're using `IndexSet` to have a stable element order when restoring persisted state, as well as
137143
// for performance when iterating over the sets.
138-
#[derive(Default)]
139144
pub struct FuzzDictionary {
140145
/// Collected state values.
141146
state_values: B256IndexSet,
@@ -173,9 +178,27 @@ impl fmt::Debug for FuzzDictionary {
173178
}
174179
}
175180

181+
impl Default for FuzzDictionary {
182+
fn default() -> Self {
183+
Self::new(Default::default())
184+
}
185+
}
186+
176187
impl FuzzDictionary {
177188
pub fn new(config: FuzzDictionaryConfig) -> Self {
178-
let mut dictionary = Self { config, samples_seeded: false, ..Default::default() };
189+
let mut dictionary = Self {
190+
config,
191+
samples_seeded: false,
192+
193+
state_values: Default::default(),
194+
addresses: Default::default(),
195+
db_state_values: Default::default(),
196+
db_addresses: Default::default(),
197+
sample_values: Default::default(),
198+
literal_values: Default::default(),
199+
misses: Default::default(),
200+
hits: Default::default(),
201+
};
179202
dictionary.prefill();
180203
dictionary
181204
}
@@ -190,7 +213,8 @@ impl FuzzDictionary {
190213
#[cold]
191214
fn seed_samples(&mut self) {
192215
trace!("seeding `sample_values` from literal dictionary");
193-
self.sample_values.extend(self.literal_values.take_words());
216+
self.sample_values
217+
.extend(self.literal_values.get().words.iter().map(|(k, v)| (k.clone(), v.clone())));
194218
self.samples_seeded = true;
195219
}
196220

0 commit comments

Comments
 (0)