Skip to content

Commit 8c17849

Browse files
authored
Adopt Rust's source-based code coverage instrumentation (#3119)
This PR replaces the line-based coverage instrumentation we introduced in #2609 with the standard source-based code coverage instrumentation performed by the Rust compiler. As a result, we now insert code coverage checks in the `StatementKind::Coverage(..)` statements produced by the Rust compiler during compilation. These checks include coverage-relevant information[^note-internal] such as the coverage counter/expression they represent [^note-instrument]. Both the coverage metadata (`kanimap`) and coverage results (`kaniraw`) are saved into files after the verification stage. Unfortunately, we currently have a chicken-egg problem with this PR and #3121, where we introduce a tool named `kani-cov` to postprocess coverage results. As explained in #3143, `kani-cov` is expected to be an alias for the `cov` subcommand and provide most of the postprocessing features for coverage-related purposes. But, the tool will likely be introduced after this change. Therefore, we propose to temporarily print a list of the regions in each function with their associated coverage status (i.e., `COVERED` or `UNCOVERED`). ### Source-based code coverage: An example The main advantage of source-based coverage results is their precision with respect to the source code. The [Source-based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html) documentation explains more details about the LLVM coverage workflow and its different options. For example, let's take this Rust code: ```rust 1 fn _other_function() { 2 println!("Hello, world!"); 3 } 4 5 fn test_cov(val: u32) -> bool { 6 if val < 3 || val == 42 { 7 true 8 } else { 9 false 10 } 11 } 12 13 #[cfg_attr(kani, kani::proof)] 14 fn main() { 15 let test1 = test_cov(1); 16 let test2 = test_cov(2); 17 assert!(test1); 18 assert!(test2); 19 } ``` Compiling and running the program with `rustc` and the `-C instrument-coverage` flag, and using the LLVM tools can get us the following coverage result: ![Image](https://github.com/model-checking/kani/assets/73246657/9070e390-6e0b-4add-828d-d9f9caacad07) In contrast, the `cargo kani --coverage -Zsource-coverage` command currently generates: ``` src/main.rs (main) * 14:1 - 19:2 COVERED src/main.rs (test_cov) * 5:1 - 6:15 COVERED * 6:19 - 6:28 UNCOVERED * 7:9 - 7:13 COVERED * 9:9 - 9:14 UNCOVERED * 11:1 - 11:2 COVERED ``` which is a verification-based coverage result almost equivalent to the runtime coverage results. ### Benchmarking We have evaluated the performance impact of the instrumentation using the `kani-perf.sh` suite (14 benchmarks). For each test, we compare the average time to run standard verification against the average time to run verification with the source-based code coverage feature enabled[^note-line-evaluation]. The evaluation has been performed on an EC2 `m5a.4xlarge` instance running Ubuntu 22.04. The experimental data has been obtained by running the `kani-perf.sh` script 10 times for each version (`only verification` and `verification + coverage`), computing the average and standard deviation. We've split this data into `small` (tests taking 60s or less) and `large` (tests taking more than 60s) and drawn the two graphs below. #### Performance comparison - `small` benchmarks ![performance_comparison_small](https://github.com/user-attachments/assets/679cf412-0193-4b0c-a78c-2d0fb702706f) #### Performance comparison - `large` benchmarks ![performance_comparison_large](https://github.com/user-attachments/assets/4bb5a895-7f57-49e0-86b5-5fea67fad939) #### Comments on performance Looking at the small tests, the performance impact seems negligible in such cases. The difference is more noticeable in the large tests, where the time to run verification and coverage can take 2x or even more. It wouldn't be surprising that, as programs become larger, the complexity of the coverage checking grows exponentially as well. However, since most verification jobs don't take longer than 30min (1800s), it's OK to say that coverage checking represents a 100-200% slowdown in the worst case w.r.t. standard verification. It's also worth noting a few other things: * The standard deviation remains similar in most cases, meaning that the coverage feature doesn't have an impact on their stability. * We haven't tried any SAT solvers other than the ones used by default for each benchmark. It's possible that other solvers perform better/worse with the coverage feature enabled. ### Call-outs * The soundness issue documented in #3441. * The issue with saving coverage mappings for non-reachable functions documented in #3445. * I've modified the test cases in `tests/coverage/` to test this feature. Since this technique is simpler, we don't need that many test cases. However, it's possible I've left some test cases which don't contribute much. Please let me know if you want to add/remove a test case. [^note-internal]: The coverage mappings can't be accessed through the StableMIR interface so we retrieve them through the internal API. [^note-instrument]: The instrumentation replaces certain counters with expressions based on other counters when possible to avoid a part of the runtime overhead. More details can be found [here](https://github.com/rust-lang/rustc-dev-guide/blob/master/src/llvm-coverage-instrumentation.md#mir-pass-instrumentcoverage). Unfortunately, we can't avoid instrumenting expressions at the moment. [^note-line-evaluation]: We have not compared performance against the line-based code coverage feature because it doesn't seem worth it. The line-based coverage feature is guaranteed to include more coverage checks than the source-based one for any function. In addition, source-based results are more precise than line-based ones. So this change represents both a quantitative and qualitative improvement. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent 7a02955 commit 8c17849

File tree

105 files changed

+950
-554
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+950
-554
lines changed

Cargo.lock

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ dependencies = [
316316
"memchr",
317317
]
318318

319+
[[package]]
320+
name = "deranged"
321+
version = "0.3.11"
322+
source = "registry+https://github.com/rust-lang/crates.io-index"
323+
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
324+
dependencies = [
325+
"powerfmt",
326+
]
327+
319328
[[package]]
320329
name = "either"
321330
version = "1.13.0"
@@ -500,6 +509,7 @@ dependencies = [
500509
"strum",
501510
"strum_macros",
502511
"tempfile",
512+
"time",
503513
"toml",
504514
"tracing",
505515
"tracing-subscriber",
@@ -660,6 +670,12 @@ dependencies = [
660670
"num-traits",
661671
]
662672

673+
[[package]]
674+
name = "num-conv"
675+
version = "0.1.0"
676+
source = "registry+https://github.com/rust-lang/crates.io-index"
677+
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
678+
663679
[[package]]
664680
name = "num-integer"
665681
version = "0.1.46"
@@ -767,6 +783,12 @@ version = "0.2.14"
767783
source = "registry+https://github.com/rust-lang/crates.io-index"
768784
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
769785

786+
[[package]]
787+
name = "powerfmt"
788+
version = "0.2.0"
789+
source = "registry+https://github.com/rust-lang/crates.io-index"
790+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
791+
770792
[[package]]
771793
name = "ppv-lite86"
772794
version = "0.2.20"
@@ -1179,6 +1201,37 @@ dependencies = [
11791201
"once_cell",
11801202
]
11811203

1204+
[[package]]
1205+
name = "time"
1206+
version = "0.3.36"
1207+
source = "registry+https://github.com/rust-lang/crates.io-index"
1208+
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
1209+
dependencies = [
1210+
"deranged",
1211+
"itoa",
1212+
"num-conv",
1213+
"powerfmt",
1214+
"serde",
1215+
"time-core",
1216+
"time-macros",
1217+
]
1218+
1219+
[[package]]
1220+
name = "time-core"
1221+
version = "0.1.2"
1222+
source = "registry+https://github.com/rust-lang/crates.io-index"
1223+
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
1224+
1225+
[[package]]
1226+
name = "time-macros"
1227+
version = "0.2.18"
1228+
source = "registry+https://github.com/rust-lang/crates.io-index"
1229+
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
1230+
dependencies = [
1231+
"num-conv",
1232+
"time-core",
1233+
]
1234+
11821235
[[package]]
11831236
name = "toml"
11841237
version = "0.8.19"

kani-compiler/src/codegen_cprover_gotoc/codegen/assert.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use crate::codegen_cprover_gotoc::GotocCtx;
2222
use cbmc::goto_program::{Expr, Location, Stmt, Type};
2323
use cbmc::InternedString;
24+
use rustc_middle::mir::coverage::CodeRegion;
2425
use stable_mir::mir::{Place, ProjectionElem};
2526
use stable_mir::ty::{Span as SpanStable, TypeAndMut};
2627
use strum_macros::{AsRefStr, EnumString};
@@ -148,18 +149,19 @@ impl<'tcx> GotocCtx<'tcx> {
148149
}
149150

150151
/// Generate a cover statement for code coverage reports.
151-
pub fn codegen_coverage(&self, span: SpanStable) -> Stmt {
152+
pub fn codegen_coverage(
153+
&self,
154+
counter_data: &str,
155+
span: SpanStable,
156+
code_region: CodeRegion,
157+
) -> Stmt {
152158
let loc = self.codegen_caller_span_stable(span);
153159
// Should use Stmt::cover, but currently this doesn't work with CBMC
154160
// unless it is run with '--cover cover' (see
155161
// https://github.com/diffblue/cbmc/issues/6613). So for now use
156162
// `assert(false)`.
157-
self.codegen_assert(
158-
Expr::bool_false(),
159-
PropertyClass::CodeCoverage,
160-
"code coverage for location",
161-
loc,
162-
)
163+
let msg = format!("{counter_data} - {code_region:?}");
164+
self.codegen_assert(Expr::bool_false(), PropertyClass::CodeCoverage, &msg, loc)
163165
}
164166

165167
// The above represent the basic operations we can perform w.r.t. assert/assume/cover

kani-compiler/src/codegen_cprover_gotoc/codegen/block.rs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,53 +20,24 @@ impl<'tcx> GotocCtx<'tcx> {
2020
pub fn codegen_block(&mut self, bb: BasicBlockIdx, bbd: &BasicBlock) {
2121
debug!(?bb, "codegen_block");
2222
let label = bb_label(bb);
23-
let check_coverage = self.queries.args().check_coverage;
2423
// the first statement should be labelled. if there is no statements, then the
2524
// terminator should be labelled.
2625
match bbd.statements.len() {
2726
0 => {
2827
let term = &bbd.terminator;
2928
let tcode = self.codegen_terminator(term);
30-
// When checking coverage, the `coverage` check should be
31-
// labelled instead.
32-
if check_coverage {
33-
let span = term.span;
34-
let cover = self.codegen_coverage(span);
35-
self.current_fn_mut().push_onto_block(cover.with_label(label));
36-
self.current_fn_mut().push_onto_block(tcode);
37-
} else {
38-
self.current_fn_mut().push_onto_block(tcode.with_label(label));
39-
}
29+
self.current_fn_mut().push_onto_block(tcode.with_label(label));
4030
}
4131
_ => {
4232
let stmt = &bbd.statements[0];
4333
let scode = self.codegen_statement(stmt);
44-
// When checking coverage, the `coverage` check should be
45-
// labelled instead.
46-
if check_coverage {
47-
let span = stmt.span;
48-
let cover = self.codegen_coverage(span);
49-
self.current_fn_mut().push_onto_block(cover.with_label(label));
50-
self.current_fn_mut().push_onto_block(scode);
51-
} else {
52-
self.current_fn_mut().push_onto_block(scode.with_label(label));
53-
}
34+
self.current_fn_mut().push_onto_block(scode.with_label(label));
5435

5536
for s in &bbd.statements[1..] {
56-
if check_coverage {
57-
let span = s.span;
58-
let cover = self.codegen_coverage(span);
59-
self.current_fn_mut().push_onto_block(cover);
60-
}
6137
let stmt = self.codegen_statement(s);
6238
self.current_fn_mut().push_onto_block(stmt);
6339
}
6440
let term = &bbd.terminator;
65-
if check_coverage {
66-
let span = term.span;
67-
let cover = self.codegen_coverage(span);
68-
self.current_fn_mut().push_onto_block(cover);
69-
}
7041
let tcode = self.codegen_terminator(term);
7142
self.current_fn_mut().push_onto_block(tcode);
7243
}

kani-compiler/src/codegen_cprover_gotoc/codegen/function.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,74 @@ impl<'tcx> GotocCtx<'tcx> {
216216
self.reset_current_fn();
217217
}
218218
}
219+
220+
pub mod rustc_smir {
221+
use crate::stable_mir::CrateDef;
222+
use rustc_middle::mir::coverage::CodeRegion;
223+
use rustc_middle::mir::coverage::CovTerm;
224+
use rustc_middle::mir::coverage::MappingKind::Code;
225+
use rustc_middle::ty::TyCtxt;
226+
use stable_mir::mir::mono::Instance;
227+
use stable_mir::Opaque;
228+
229+
type CoverageOpaque = stable_mir::Opaque;
230+
231+
/// Retrieves the `CodeRegion` associated with the data in a
232+
/// `CoverageOpaque` object.
233+
pub fn region_from_coverage_opaque(
234+
tcx: TyCtxt,
235+
coverage_opaque: &CoverageOpaque,
236+
instance: Instance,
237+
) -> Option<CodeRegion> {
238+
let cov_term = parse_coverage_opaque(coverage_opaque);
239+
region_from_coverage(tcx, cov_term, instance)
240+
}
241+
242+
/// Retrieves the `CodeRegion` associated with a `CovTerm` object.
243+
///
244+
/// Note: This function could be in the internal `rustc` impl for `Coverage`.
245+
pub fn region_from_coverage(
246+
tcx: TyCtxt<'_>,
247+
coverage: CovTerm,
248+
instance: Instance,
249+
) -> Option<CodeRegion> {
250+
// We need to pull the coverage info from the internal MIR instance.
251+
let instance_def = rustc_smir::rustc_internal::internal(tcx, instance.def.def_id());
252+
let body = tcx.instance_mir(rustc_middle::ty::InstanceKind::Item(instance_def));
253+
254+
// Some functions, like `std` ones, may not have coverage info attached
255+
// to them because they have been compiled without coverage flags.
256+
if let Some(cov_info) = &body.function_coverage_info {
257+
// Iterate over the coverage mappings and match with the coverage term.
258+
for mapping in &cov_info.mappings {
259+
let Code(term) = mapping.kind else { unreachable!() };
260+
if term == coverage {
261+
return Some(mapping.code_region.clone());
262+
}
263+
}
264+
}
265+
None
266+
}
267+
268+
/// Parse a `CoverageOpaque` item and return the corresponding `CovTerm`:
269+
/// <https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/coverage/enum.CovTerm.html>
270+
///
271+
/// At present, a `CovTerm` can be one of the following:
272+
/// - `CounterIncrement(<num>)`: A physical counter.
273+
/// - `ExpressionUsed(<num>)`: An expression-based counter.
274+
/// - `Zero`: A counter with a constant zero value.
275+
fn parse_coverage_opaque(coverage_opaque: &Opaque) -> CovTerm {
276+
let coverage_str = coverage_opaque.to_string();
277+
if let Some(rest) = coverage_str.strip_prefix("CounterIncrement(") {
278+
let (num_str, _rest) = rest.split_once(')').unwrap();
279+
let num = num_str.parse::<u32>().unwrap();
280+
CovTerm::Counter(num.into())
281+
} else if let Some(rest) = coverage_str.strip_prefix("ExpressionUsed(") {
282+
let (num_str, _rest) = rest.split_once(')').unwrap();
283+
let num = num_str.parse::<u32>().unwrap();
284+
CovTerm::Expression(num.into())
285+
} else {
286+
CovTerm::Zero
287+
}
288+
}
289+
}

kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::typ::TypeExt;
44
use super::typ::FN_RETURN_VOID_VAR_NAME;
55
use super::{bb_label, PropertyClass};
6+
use crate::codegen_cprover_gotoc::codegen::function::rustc_smir::region_from_coverage_opaque;
67
use crate::codegen_cprover_gotoc::{GotocCtx, VtableCtx};
78
use crate::unwrap_or_return_codegen_unimplemented_stmt;
89
use cbmc::goto_program::{Expr, Location, Stmt, Type};
@@ -158,12 +159,28 @@ impl<'tcx> GotocCtx<'tcx> {
158159
location,
159160
)
160161
}
162+
StatementKind::Coverage(coverage_opaque) => {
163+
let function_name = self.current_fn().readable_name();
164+
let instance = self.current_fn().instance_stable();
165+
let counter_data = format!("{coverage_opaque:?} ${function_name}$");
166+
let maybe_code_region =
167+
region_from_coverage_opaque(self.tcx, &coverage_opaque, instance);
168+
if let Some(code_region) = maybe_code_region {
169+
let coverage_stmt =
170+
self.codegen_coverage(&counter_data, stmt.span, code_region);
171+
// TODO: Avoid single-statement blocks when conversion of
172+
// standalone statements to the irep format is fixed.
173+
// More details in <https://github.com/model-checking/kani/issues/3012>
174+
Stmt::block(vec![coverage_stmt], location)
175+
} else {
176+
Stmt::skip(location)
177+
}
178+
}
161179
StatementKind::PlaceMention(_) => todo!(),
162180
StatementKind::FakeRead(..)
163181
| StatementKind::Retag(_, _)
164182
| StatementKind::AscribeUserType { .. }
165183
| StatementKind::Nop
166-
| StatementKind::Coverage { .. }
167184
| StatementKind::ConstEvalCounter => Stmt::skip(location),
168185
}
169186
.with_location(location)

kani-driver/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ tracing = {version = "0.1", features = ["max_level_trace", "release_max_level_de
3434
tracing-subscriber = {version = "0.3.8", features = ["env-filter", "json", "fmt"]}
3535
rand = "0.8"
3636
which = "6"
37+
time = {version = "0.3.36", features = ["formatting"]}
3738

3839
# A good set of suggested dependencies can be found in rustup:
3940
# https://github.com/rust-lang/rustup/blob/master/Cargo.toml

kani-driver/src/args/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,12 +615,12 @@ impl ValidateArgs for VerificationArgs {
615615
}
616616

617617
if self.coverage
618-
&& !self.common_args.unstable_features.contains(UnstableFeature::LineCoverage)
618+
&& !self.common_args.unstable_features.contains(UnstableFeature::SourceCoverage)
619619
{
620620
return Err(Error::raw(
621621
ErrorKind::MissingRequiredArgument,
622622
"The `--coverage` argument is unstable and requires `-Z \
623-
line-coverage` to be used.",
623+
source-coverage` to be used.",
624624
));
625625
}
626626

kani-driver/src/call_cargo.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ impl KaniSession {
206206
})
207207
}
208208

209-
fn cargo_metadata(&self, build_target: &str) -> Result<Metadata> {
209+
pub fn cargo_metadata(&self, build_target: &str) -> Result<Metadata> {
210210
let mut cmd = MetadataCommand::new();
211211

212212
// restrict metadata command to host platform. References:

0 commit comments

Comments
 (0)