Skip to content

Commit b7a065f

Browse files
authored
perf(coverage): improve HitMap merging and internal repr (#9456)
1 parent 7d0b0a0 commit b7a065f

File tree

3 files changed

+50
-47
lines changed

3 files changed

+50
-47
lines changed

crates/evm/coverage/src/lib.rs

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
55
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
66
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
77

8-
#[macro_use]
9-
extern crate foundry_common;
10-
118
#[macro_use]
129
extern crate tracing;
1310

14-
use alloy_primitives::{map::HashMap, Bytes, B256};
15-
use eyre::{Context, Result};
11+
use alloy_primitives::{
12+
map::{B256HashMap, HashMap},
13+
Bytes,
14+
};
15+
use eyre::Result;
1616
use foundry_compilers::artifacts::sourcemap::SourceMap;
1717
use semver::Version;
1818
use std::{
1919
collections::BTreeMap,
2020
fmt::Display,
21+
num::NonZeroU32,
2122
ops::{Deref, DerefMut, Range},
2223
path::{Path, PathBuf},
2324
sync::Arc,
@@ -119,22 +120,21 @@ impl CoverageReport {
119120
is_deployed_code: bool,
120121
) -> Result<()> {
121122
// Add bytecode level hits
122-
let e = self
123-
.bytecode_hits
123+
self.bytecode_hits
124124
.entry(contract_id.clone())
125-
.or_insert_with(|| HitMap::new(hit_map.bytecode.clone()));
126-
e.merge(hit_map).wrap_err_with(|| format!("{contract_id:?}"))?;
125+
.and_modify(|m| m.merge(hit_map))
126+
.or_insert_with(|| hit_map.clone());
127127

128128
// Add source level hits
129129
if let Some(anchors) = self.anchors.get(contract_id) {
130130
let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
131131
for anchor in anchors {
132-
if let Some(&hits) = hit_map.hits.get(&anchor.instruction) {
132+
if let Some(hits) = hit_map.get(anchor.instruction) {
133133
self.items
134134
.get_mut(&contract_id.version)
135135
.and_then(|items| items.get_mut(anchor.item_id))
136136
.expect("Anchor refers to non-existent coverage item")
137-
.hits += hits;
137+
.hits += hits.get();
138138
}
139139
}
140140
}
@@ -160,9 +160,10 @@ impl CoverageReport {
160160

161161
/// A collection of [`HitMap`]s.
162162
#[derive(Clone, Debug, Default)]
163-
pub struct HitMaps(pub HashMap<B256, HitMap>);
163+
pub struct HitMaps(pub B256HashMap<HitMap>);
164164

165165
impl HitMaps {
166+
/// Merges two `Option<HitMaps>`.
166167
pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
167168
match (a, b) {
168169
(_, None) => {}
@@ -171,25 +172,22 @@ impl HitMaps {
171172
}
172173
}
173174

175+
/// Merges two `HitMaps`.
174176
pub fn merge(&mut self, other: Self) {
175-
for (code_hash, hit_map) in other.0 {
176-
if let Some(HitMap { hits: extra_hits, .. }) = self.insert(code_hash, hit_map) {
177-
for (pc, hits) in extra_hits {
178-
self.entry(code_hash)
179-
.and_modify(|map| *map.hits.entry(pc).or_default() += hits);
180-
}
181-
}
177+
for (code_hash, other) in other.0 {
178+
self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
182179
}
183180
}
184181

182+
/// Merges two `HitMaps`.
185183
pub fn merged(mut self, other: Self) -> Self {
186184
self.merge(other);
187185
self
188186
}
189187
}
190188

191189
impl Deref for HitMaps {
192-
type Target = HashMap<B256, HitMap>;
190+
type Target = B256HashMap<HitMap>;
193191

194192
fn deref(&self) -> &Self::Target {
195193
&self.0
@@ -207,40 +205,46 @@ impl DerefMut for HitMaps {
207205
/// Contains low-level data about hit counters for the instructions in the bytecode of a contract.
208206
#[derive(Clone, Debug)]
209207
pub struct HitMap {
210-
pub bytecode: Bytes,
211-
pub hits: BTreeMap<usize, u64>,
208+
bytecode: Bytes,
209+
hits: HashMap<u32, u32>,
212210
}
213211

214212
impl HitMap {
213+
/// Create a new hitmap with the given bytecode.
215214
pub fn new(bytecode: Bytes) -> Self {
216-
Self { bytecode, hits: BTreeMap::new() }
215+
Self { bytecode, hits: Default::default() }
216+
}
217+
218+
/// Returns the bytecode.
219+
pub fn bytecode(&self) -> &Bytes {
220+
&self.bytecode
217221
}
218222

219-
/// Increase the hit counter for the given program counter.
223+
/// Returns the number of hits for the given program counter.
224+
pub fn get(&self, pc: usize) -> Option<NonZeroU32> {
225+
NonZeroU32::new(self.hits.get(&Self::cvt_pc(pc)).copied().unwrap_or(0))
226+
}
227+
228+
/// Increase the hit counter by 1 for the given program counter.
220229
pub fn hit(&mut self, pc: usize) {
221-
*self.hits.entry(pc).or_default() += 1;
230+
self.hits(pc, 1)
231+
}
232+
233+
/// Increase the hit counter by `hits` for the given program counter.
234+
pub fn hits(&mut self, pc: usize, hits: u32) {
235+
*self.hits.entry(Self::cvt_pc(pc)).or_default() += hits;
222236
}
223237

224238
/// Merge another hitmap into this, assuming the bytecode is consistent
225-
pub fn merge(&mut self, other: &Self) -> Result<(), eyre::Report> {
226-
for (pc, hits) in &other.hits {
227-
*self.hits.entry(*pc).or_default() += hits;
239+
pub fn merge(&mut self, other: &Self) {
240+
for (&pc, &hits) in &other.hits {
241+
self.hits(pc as usize, hits);
228242
}
229-
Ok(())
230243
}
231244

232-
pub fn consistent_bytecode(&self, hm1: &Self, hm2: &Self) -> bool {
233-
// Consider the bytecodes consistent if they are the same out as far as the
234-
// recorded hits
235-
let len1 = hm1.hits.last_key_value();
236-
let len2 = hm2.hits.last_key_value();
237-
if let (Some(len1), Some(len2)) = (len1, len2) {
238-
let len = std::cmp::max(len1.0, len2.0);
239-
let ok = hm1.bytecode.0[..*len] == hm2.bytecode.0[..*len];
240-
let _ = sh_println!("consistent_bytecode: {}, {}, {}, {}", ok, len1.0, len2.0, len);
241-
return ok;
242-
}
243-
true
245+
#[inline]
246+
fn cvt_pc(pc: usize) -> u32 {
247+
pc.try_into().expect("4GiB bytecode")
244248
}
245249
}
246250

@@ -311,7 +315,7 @@ pub struct CoverageItem {
311315
/// The location of the item in the source code.
312316
pub loc: SourceLocation,
313317
/// The number of times this item was hit.
314-
pub hits: u64,
318+
pub hits: u32,
315319
}
316320

317321
impl Display for CoverageItem {

crates/forge/bin/cmd/coverage.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,10 @@ impl CoverageArgs {
250250
for result in suite.test_results.values() {
251251
let Some(hit_maps) = result.coverage.as_ref() else { continue };
252252
for map in hit_maps.0.values() {
253-
if let Some((id, _)) = known_contracts.find_by_deployed_code(&map.bytecode) {
253+
if let Some((id, _)) = known_contracts.find_by_deployed_code(map.bytecode()) {
254254
hits.push((id, map, true));
255255
} else if let Some((id, _)) =
256-
known_contracts.find_by_creation_code(&map.bytecode)
256+
known_contracts.find_by_creation_code(map.bytecode())
257257
{
258258
hits.push((id, map, false));
259259
}

crates/forge/src/coverage.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,15 @@ impl CoverageReporter for BytecodeReporter {
212212
let mut line_number_cache = LineNumberCache::new(self.root.clone());
213213

214214
for (contract_id, hits) in &report.bytecode_hits {
215-
let ops = disassemble_bytes(hits.bytecode.to_vec())?;
215+
let ops = disassemble_bytes(hits.bytecode().to_vec())?;
216216
let mut formatted = String::new();
217217

218218
let source_elements =
219219
report.source_maps.get(contract_id).map(|sm| &sm.1).unwrap_or(&no_source_elements);
220220

221221
for (code, source_element) in std::iter::zip(ops.iter(), source_elements) {
222222
let hits = hits
223-
.hits
224-
.get(&(code.offset as usize))
223+
.get(code.offset as usize)
225224
.map(|h| format!("[{h:03}]"))
226225
.unwrap_or(" ".to_owned());
227226
let source_id = source_element.index();

0 commit comments

Comments
 (0)