Skip to content

Commit 22202a7

Browse files
authored
perf(coverage): cache current HitMap, reserve when merging (#9469)
* perf(coverage): cache current HitMap, reserve when merging * test: shuffle RPC env
1 parent 2f56133 commit 22202a7

File tree

2 files changed

+91
-16
lines changed

2 files changed

+91
-16
lines changed
Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
use crate::{HitMap, HitMaps};
22
use alloy_primitives::B256;
33
use revm::{interpreter::Interpreter, Database, EvmContext, Inspector};
4+
use std::ptr::NonNull;
45

56
/// Inspector implementation for collecting coverage information.
6-
#[derive(Clone, Debug, Default)]
7+
#[derive(Clone, Debug)]
78
pub struct CoverageCollector {
9+
current_map: NonNull<HitMap>,
10+
current_hash: B256,
811
maps: HitMaps,
912
}
1013

14+
// SAFETY: `current_map` is always valid and points into an allocation managed by self.
15+
unsafe impl Send for CoverageCollector {}
16+
unsafe impl Sync for CoverageCollector {}
17+
18+
impl Default for CoverageCollector {
19+
fn default() -> Self {
20+
Self {
21+
current_map: NonNull::dangling(),
22+
current_hash: B256::ZERO,
23+
maps: Default::default(),
24+
}
25+
}
26+
}
27+
1128
impl<DB: Database> Inspector<DB> for CoverageCollector {
1229
fn initialize_interp(&mut self, interpreter: &mut Interpreter, _context: &mut EvmContext<DB>) {
13-
self.maps
14-
.entry(*get_contract_hash(interpreter))
15-
.or_insert_with(|| HitMap::new(interpreter.contract.bytecode.original_bytes()));
30+
get_or_insert_contract_hash(interpreter);
31+
self.insert_map(interpreter);
1632
}
1733

1834
#[inline]
1935
fn step(&mut self, interpreter: &mut Interpreter, _context: &mut EvmContext<DB>) {
20-
if let Some(map) = self.maps.get_mut(get_contract_hash(interpreter)) {
21-
map.hit(interpreter.program_counter());
22-
}
36+
let map = self.get_or_insert_map(interpreter);
37+
map.hit(interpreter.program_counter());
2338
}
2439
}
2540

@@ -28,15 +43,50 @@ impl CoverageCollector {
2843
pub fn finish(self) -> HitMaps {
2944
self.maps
3045
}
46+
47+
#[inline]
48+
fn get_or_insert_map(&mut self, interpreter: &mut Interpreter) -> &mut HitMap {
49+
let hash = get_or_insert_contract_hash(interpreter);
50+
if self.current_hash != *hash {
51+
self.insert_map(interpreter);
52+
}
53+
unsafe { self.current_map.as_mut() }
54+
}
55+
56+
#[cold]
57+
#[inline(never)]
58+
fn insert_map(&mut self, interpreter: &Interpreter) {
59+
let Some(hash) = interpreter.contract.hash else { eof_panic() };
60+
self.current_hash = hash;
61+
self.current_map = self
62+
.maps
63+
.entry(hash)
64+
.or_insert_with(|| HitMap::new(interpreter.contract.bytecode.original_bytes()))
65+
.into();
66+
}
3167
}
3268

3369
/// Helper function for extracting contract hash used to record coverage hit map.
34-
/// If contract hash available in interpreter contract is zero (contract not yet created but going
35-
/// to be created in current tx) then it hash is calculated from contract bytecode.
36-
fn get_contract_hash(interpreter: &mut Interpreter) -> &B256 {
37-
let hash = interpreter.contract.hash.as_mut().expect("coverage does not support EOF");
38-
if *hash == B256::ZERO {
39-
*hash = interpreter.contract.bytecode.hash_slow();
70+
///
71+
/// If the contract hash is zero (contract not yet created but it's going to be created in current
72+
/// tx) then the hash is calculated from the bytecode.
73+
#[inline]
74+
fn get_or_insert_contract_hash(interpreter: &mut Interpreter) -> &B256 {
75+
let Some(hash) = interpreter.contract.hash.as_mut() else { eof_panic() };
76+
if hash.is_zero() {
77+
set_contract_hash(hash, &interpreter.contract.bytecode);
4078
}
4179
hash
4280
}
81+
82+
#[cold]
83+
#[inline(never)]
84+
fn set_contract_hash(hash: &mut B256, bytecode: &revm::primitives::Bytecode) {
85+
*hash = bytecode.hash_slow();
86+
}
87+
88+
#[cold]
89+
#[inline(never)]
90+
fn eof_panic() -> ! {
91+
panic!("coverage does not support EOF");
92+
}

crates/evm/coverage/src/lib.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ impl HitMaps {
174174

175175
/// Merges two `HitMaps`.
176176
pub fn merge(&mut self, other: Self) {
177+
self.reserve(other.len());
177178
for (code_hash, other) in other.0 {
178179
self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
179180
}
@@ -211,37 +212,61 @@ pub struct HitMap {
211212

212213
impl HitMap {
213214
/// Create a new hitmap with the given bytecode.
215+
#[inline]
214216
pub fn new(bytecode: Bytes) -> Self {
215-
Self { bytecode, hits: Default::default() }
217+
Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
216218
}
217219

218220
/// Returns the bytecode.
221+
#[inline]
219222
pub fn bytecode(&self) -> &Bytes {
220223
&self.bytecode
221224
}
222225

223226
/// Returns the number of hits for the given program counter.
227+
#[inline]
224228
pub fn get(&self, pc: usize) -> Option<NonZeroU32> {
225229
NonZeroU32::new(self.hits.get(&Self::cvt_pc(pc)).copied().unwrap_or(0))
226230
}
227231

228232
/// Increase the hit counter by 1 for the given program counter.
233+
#[inline]
229234
pub fn hit(&mut self, pc: usize) {
230235
self.hits(pc, 1)
231236
}
232237

233238
/// Increase the hit counter by `hits` for the given program counter.
239+
#[inline]
234240
pub fn hits(&mut self, pc: usize, hits: u32) {
235241
*self.hits.entry(Self::cvt_pc(pc)).or_default() += hits;
236242
}
237243

238244
/// Merge another hitmap into this, assuming the bytecode is consistent
239245
pub fn merge(&mut self, other: &Self) {
240-
for (&pc, &hits) in &other.hits {
241-
self.hits(pc as usize, hits);
246+
self.hits.reserve(other.len());
247+
for (pc, hits) in other.iter() {
248+
self.hits(pc, hits);
242249
}
243250
}
244251

252+
/// Returns an iterator over all the program counters and their hit counts.
253+
#[inline]
254+
pub fn iter(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
255+
self.hits.iter().map(|(&pc, &hits)| (pc as usize, hits))
256+
}
257+
258+
/// Returns the number of program counters hit in the hitmap.
259+
#[inline]
260+
pub fn len(&self) -> usize {
261+
self.hits.len()
262+
}
263+
264+
/// Returns `true` if the hitmap is empty.
265+
#[inline]
266+
pub fn is_empty(&self) -> bool {
267+
self.hits.is_empty()
268+
}
269+
245270
#[inline]
246271
fn cvt_pc(pc: usize) -> u32 {
247272
pc.try_into().expect("4GiB bytecode")

0 commit comments

Comments
 (0)