Skip to content

Commit 836c317

Browse files
committed
Auto merge of #83774 - richkadel:zero-based-counters, r=tmandry
Translate counters from Rust 1-based to LLVM 0-based counter ids A colleague contacted me and asked why Rust's counters start at 1, when Clangs appear to start at 0. There is a reason why Rust's internal counters start at 1 (see the docs), and I tried to keep them consistent when codegenned to LLVM's coverage mapping format. LLVM should be tolerant of missing counters, but as my colleague pointed out, `llvm-cov` will silently fail to generate a coverage report for a function based on LLVM's assumption that the counters are 0-based. See: https://github.com/llvm/llvm-project/blob/main/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp#L170 Apparently, if, for example, a function has no branches, it would have exactly 1 counter. `CounterValues.size()` would be 1, and (with the 1-based index), the counter ID would be 1. This would fail the check and abort reporting coverage for the function. It turns out that by correcting for this during coverage map generation, by subtracting 1 from the Rust Counter ID (both when generating the counter increment intrinsic call, and when adding counters to the map), some uncovered functions (including in tests) now appear covered! This corrects the coverage for a few tests! r? `@tmandry` FYI: `@wesleywiser`
2 parents cb17136 + 7ceff68 commit 836c317

File tree

11 files changed

+212
-96
lines changed

11 files changed

+212
-96
lines changed

compiler/rustc_codegen_llvm/src/coverageinfo/mod.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,9 @@ fn add_unused_function_coverage(
250250
// Insert at least one real counter so the LLVM CoverageMappingReader will find expected
251251
// definitions.
252252
function_coverage.add_counter(UNUSED_FUNCTION_COUNTER_ID, code_region.clone());
253+
} else {
254+
function_coverage.add_unreachable_region(code_region.clone());
253255
}
254-
// Add a Zero Counter for every code region.
255-
//
256-
// Even though the first coverage region already has an actual Counter, `llvm-cov` will not
257-
// always report it. Re-adding an unreachable region (zero counter) for the same region
258-
// seems to help produce the expected coverage.
259-
function_coverage.add_unreachable_region(code_region.clone());
260256
}
261257

262258
if let Some(coverage_context) = cx.coverage_context() {

compiler/rustc_codegen_ssa/src/coverageinfo/ffi.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,39 @@ pub enum CounterKind {
2424
pub struct Counter {
2525
// Important: The layout (order and types of fields) must match its C++ counterpart.
2626
pub kind: CounterKind,
27-
pub id: u32,
27+
id: u32,
2828
}
2929

3030
impl Counter {
31+
/// Constructs a new `Counter` of kind `Zero`. For this `CounterKind`, the
32+
/// `id` is not used.
3133
pub fn zero() -> Self {
3234
Self { kind: CounterKind::Zero, id: 0 }
3335
}
3436

37+
/// Constructs a new `Counter` of kind `CounterValueReference`, and converts
38+
/// the given 1-based counter_id to the required 0-based equivalent for
39+
/// the `Counter` encoding.
3540
pub fn counter_value_reference(counter_id: CounterValueReference) -> Self {
36-
Self { kind: CounterKind::CounterValueReference, id: counter_id.into() }
41+
Self { kind: CounterKind::CounterValueReference, id: counter_id.zero_based_index() }
3742
}
3843

44+
/// Constructs a new `Counter` of kind `Expression`.
3945
pub fn expression(mapped_expression_index: MappedExpressionIndex) -> Self {
4046
Self { kind: CounterKind::Expression, id: mapped_expression_index.into() }
4147
}
48+
49+
/// Returns true if the `Counter` kind is `Zero`.
50+
pub fn is_zero(&self) -> bool {
51+
matches!(self.kind, CounterKind::Zero)
52+
}
53+
54+
/// An explicitly-named function to get the ID value, making it more obvious
55+
/// that the stored value is now 0-based.
56+
pub fn zero_based_id(&self) -> u32 {
57+
debug_assert!(!self.is_zero(), "`id` is undefined for CounterKind::Zero");
58+
self.id
59+
}
4260
}
4361

4462
/// Aligns with [llvm::coverage::CounterExpression::ExprKind](https://github.com/rust-lang/llvm-project/blob/rustc/11.0-2020-10-12/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h#L147)

compiler/rustc_codegen_ssa/src/coverageinfo/map.rs

+63-14
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,7 @@ impl<'tcx> FunctionCoverage<'tcx> {
163163
self.counters.iter_enumerated().filter_map(|(index, entry)| {
164164
// Option::map() will return None to filter out missing counters. This may happen
165165
// if, for example, a MIR-instrumented counter is removed during an optimization.
166-
entry.as_ref().map(|region| {
167-
(Counter::counter_value_reference(index as CounterValueReference), region)
168-
})
166+
entry.as_ref().map(|region| (Counter::counter_value_reference(index), region))
169167
})
170168
}
171169

@@ -206,9 +204,15 @@ impl<'tcx> FunctionCoverage<'tcx> {
206204
if id == ExpressionOperandId::ZERO {
207205
Some(Counter::zero())
208206
} else if id.index() < self.counters.len() {
207+
debug_assert!(
208+
id.index() > 0,
209+
"ExpressionOperandId indexes for counters are 1-based, but this id={}",
210+
id.index()
211+
);
209212
// Note: Some codegen-injected Counters may be only referenced by `Expression`s,
210213
// and may not have their own `CodeRegion`s,
211214
let index = CounterValueReference::from(id.index());
215+
// Note, the conversion to LLVM `Counter` adjusts the index to be zero-based.
212216
Some(Counter::counter_value_reference(index))
213217
} else {
214218
let index = self.expression_index(u32::from(id));
@@ -233,19 +237,60 @@ impl<'tcx> FunctionCoverage<'tcx> {
233237
let optional_region = &expression.region;
234238
let Expression { lhs, op, rhs, .. } = *expression;
235239

236-
if let Some(Some((lhs_counter, rhs_counter))) =
237-
id_to_counter(&new_indexes, lhs).map(|lhs_counter| {
240+
if let Some(Some((lhs_counter, mut rhs_counter))) = id_to_counter(&new_indexes, lhs)
241+
.map(|lhs_counter| {
238242
id_to_counter(&new_indexes, rhs).map(|rhs_counter| (lhs_counter, rhs_counter))
239243
})
240244
{
245+
if lhs_counter.is_zero() && op.is_subtract() {
246+
// The left side of a subtraction was probably optimized out. As an example,
247+
// a branch condition might be evaluated as a constant expression, and the
248+
// branch could be removed, dropping unused counters in the process.
249+
//
250+
// Since counters are unsigned, we must assume the result of the expression
251+
// can be no more and no less than zero. An expression known to evaluate to zero
252+
// does not need to be added to the coverage map.
253+
//
254+
// Coverage test `loops_branches.rs` includes multiple variations of branches
255+
// based on constant conditional (literal `true` or `false`), and demonstrates
256+
// that the expected counts are still correct.
257+
debug!(
258+
"Expression subtracts from zero (assume unreachable): \
259+
original_index={:?}, lhs={:?}, op={:?}, rhs={:?}, region={:?}",
260+
original_index, lhs, op, rhs, optional_region,
261+
);
262+
rhs_counter = Counter::zero();
263+
}
241264
debug_assert!(
242-
(lhs_counter.id as usize)
243-
< usize::max(self.counters.len(), self.expressions.len())
265+
lhs_counter.is_zero()
266+
// Note: with `as usize` the ID _could_ overflow/wrap if `usize = u16`
267+
|| ((lhs_counter.zero_based_id() as usize)
268+
<= usize::max(self.counters.len(), self.expressions.len())),
269+
"lhs id={} > both counters.len()={} and expressions.len()={}
270+
({:?} {:?} {:?})",
271+
lhs_counter.zero_based_id(),
272+
self.counters.len(),
273+
self.expressions.len(),
274+
lhs_counter,
275+
op,
276+
rhs_counter,
244277
);
278+
245279
debug_assert!(
246-
(rhs_counter.id as usize)
247-
< usize::max(self.counters.len(), self.expressions.len())
280+
rhs_counter.is_zero()
281+
// Note: with `as usize` the ID _could_ overflow/wrap if `usize = u16`
282+
|| ((rhs_counter.zero_based_id() as usize)
283+
<= usize::max(self.counters.len(), self.expressions.len())),
284+
"rhs id={} > both counters.len()={} and expressions.len()={}
285+
({:?} {:?} {:?})",
286+
rhs_counter.zero_based_id(),
287+
self.counters.len(),
288+
self.expressions.len(),
289+
lhs_counter,
290+
op,
291+
rhs_counter,
248292
);
293+
249294
// Both operands exist. `Expression` operands exist in `self.expressions` and have
250295
// been assigned a `new_index`.
251296
let mapped_expression_index =
@@ -268,11 +313,15 @@ impl<'tcx> FunctionCoverage<'tcx> {
268313
expression_regions.push((Counter::expression(mapped_expression_index), region));
269314
}
270315
} else {
271-
debug!(
272-
"Ignoring expression with one or more missing operands: \
273-
original_index={:?}, lhs={:?}, op={:?}, rhs={:?}, region={:?}",
274-
original_index, lhs, op, rhs, optional_region,
275-
)
316+
bug!(
317+
"expression has one or more missing operands \
318+
original_index={:?}, lhs={:?}, op={:?}, rhs={:?}, region={:?}",
319+
original_index,
320+
lhs,
321+
op,
322+
rhs,
323+
optional_region,
324+
);
276325
}
277326
}
278327
(counter_expressions, expression_regions.into_iter())

compiler/rustc_codegen_ssa/src/mir/coverageinfo.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
3636
let fn_name = bx.get_pgo_func_name_var(instance);
3737
let hash = bx.const_u64(function_source_hash);
3838
let num_counters = bx.const_u32(coverageinfo.num_counters);
39-
let index = bx.const_u32(u32::from(id));
39+
let index = bx.const_u32(id.zero_based_index());
4040
debug!(
4141
"codegen intrinsic instrprof.increment(fn_name={:?}, hash={:?}, num_counters={:?}, index={:?})",
4242
fn_name, hash, num_counters, index,

compiler/rustc_middle/src/mir/coverage.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,16 @@ rustc_index::newtype_index! {
4141
}
4242

4343
impl CounterValueReference {
44-
// Counters start at 1 to reserve 0 for ExpressionOperandId::ZERO.
44+
/// Counters start at 1 to reserve 0 for ExpressionOperandId::ZERO.
4545
pub const START: Self = Self::from_u32(1);
46+
47+
/// Returns explicitly-requested zero-based version of the counter id, used
48+
/// during codegen. LLVM expects zero-based indexes.
49+
pub fn zero_based_index(&self) -> u32 {
50+
let one_based_index = self.as_u32();
51+
debug_assert!(one_based_index > 0);
52+
one_based_index - 1
53+
}
4654
}
4755

4856
rustc_index::newtype_index! {
@@ -175,3 +183,13 @@ pub enum Op {
175183
Subtract,
176184
Add,
177185
}
186+
187+
impl Op {
188+
pub fn is_add(&self) -> bool {
189+
matches!(self, Self::Add)
190+
}
191+
192+
pub fn is_subtract(&self) -> bool {
193+
matches!(self, Self::Subtract)
194+
}
195+
}

src/test/run-make-fulldeps/coverage-reports/expected_show_coverage.async.txt

+10-17
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
10| | }
1111
11| 1|}
1212
12| |
13-
13| |async fn d() -> u8 { 1 } // should have a coverage count `0` (see below)
13+
13| 0|async fn d() -> u8 { 1 }
1414
14| |
1515
15| 0|async fn e() -> u8 { 1 } // unused function; executor does not block on `g()`
1616
16| |
@@ -66,7 +66,8 @@
6666
63| 1| 0
6767
64| 1| }
6868
65| 1| }
69-
66| 1| fn d() -> u8 { 1 }
69+
66| 1| fn d() -> u8 { 1 } // inner function is defined in-line, but the function is not executed
70+
^0
7071
67| 1| fn f() -> u8 { 1 }
7172
68| 1| match x {
7273
69| 1| y if c(x) == y + 1 => { d(); }
@@ -115,11 +116,14 @@
115116
109| |
116117
110| 1| pub fn block_on<F: Future>(mut future: F) -> F::Output {
117118
111| 1| let mut future = unsafe { Pin::new_unchecked(&mut future) };
118-
112| 1|
119+
112| 1| use std::hint::unreachable_unchecked;
119120
113| 1| static VTABLE: RawWakerVTable = RawWakerVTable::new(
120-
114| 1| |_| unimplemented!("clone"),
121-
115| 1| |_| unimplemented!("wake"),
122-
116| 1| |_| unimplemented!("wake_by_ref"),
121+
114| 1| |_| unsafe { unreachable_unchecked() }, // clone
122+
^0
123+
115| 1| |_| unsafe { unreachable_unchecked() }, // wake
124+
^0
125+
116| 1| |_| unsafe { unreachable_unchecked() }, // wake_by_ref
126+
^0
123127
117| 1| |_| (),
124128
118| 1| );
125129
119| 1| let waker = unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) };
@@ -132,15 +136,4 @@
132136
126| | }
133137
127| 1| }
134138
128| |}
135-
129| |
136-
130| |// `llvm-cov show` shows no coverage results for the `d()`, even though the
137-
131| |// crate's LLVM IR shows the function exists and has an InstrProf PGO counter,
138-
132| |// and appears to be registered like all other counted functions.
139-
133| |//
140-
134| |// `llvm-cov show --debug` output shows there is at least one `Counter` for this
141-
135| |// line, but counters do not appear in the `Combined regions` section (unlike
142-
136| |// `f()`, which is similar, but does appear in `Combined regions`, and does show
143-
137| |// coverage). The only difference is, `f()` is awaited, but the call to await
144-
138| |// `d()` is not reached. (Note: `d()` will appear in coverage if the test is
145-
139| |// modified to cause it to be awaited.)
146139

src/test/run-make-fulldeps/coverage-reports/expected_show_coverage.doctest.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
46| 1|//! println!("called some_func()");
5757
47| 1|//! }
5858
48| |//!
59-
49| |//! #[derive(Debug)]
59+
49| 0|//! #[derive(Debug)]
6060
50| |//! struct SomeError;
6161
51| |//!
6262
52| |//! extern crate doctest_crate;

src/test/run-make-fulldeps/coverage-reports/expected_show_coverage.inline.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
46| 6|}
4848
47| |
4949
48| |#[inline(always)]
50-
49| |fn error() {
51-
50| | panic!("error");
52-
51| |}
50+
49| 0|fn error() {
51+
50| 0| panic!("error");
52+
51| 0|}
5353

Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
1| |#![allow(unused_assignments, unused_variables, while_true)]
22
2| |
3-
3| |// This test confirms an earlier problem was resolved, supporting the MIR graph generated by the
4-
4| |// structure of this `fmt` function.
3+
3| |// This test confirms that (1) unexecuted infinite loops are handled correctly by the
4+
4| |// InstrumentCoverage MIR pass; and (2) Counter Expressions that subtract from zero can be dropped.
55
5| |
66
6| |struct DebugTest;
77
7| |
@@ -16,23 +16,51 @@
1616
^0
1717
16| | } else {
1818
17| | }
19-
18| 1| Ok(())
20-
19| 1| }
21-
20| |}
22-
21| |
23-
22| 1|fn main() {
24-
23| 1| let debug_test = DebugTest;
25-
24| 1| println!("{:?}", debug_test);
26-
25| 1|}
27-
26| |
28-
27| |/*
29-
28| |
30-
29| |This is the error message generated, before the issue was fixed:
31-
30| |
32-
31| |error: internal compiler error: compiler/rustc_mir/src/transform/coverage/mod.rs:374:42:
33-
32| |Error processing: DefId(0:6 ~ bug_incomplete_cov_graph_traversal_simplified[317d]::{impl#0}::fmt):
34-
33| |Error { message: "`TraverseCoverageGraphWithLoops` missed some `BasicCoverageBlock`s:
35-
34| |[bcb6, bcb7, bcb9]" }
36-
35| |
37-
36| |*/
19+
18| |
20+
19| 10| for i in 0..10 {
21+
20| 10| if true {
22+
21| 10| if false {
23+
22| | while true {}
24+
23| 10| }
25+
24| 10| write!(f, "error")?;
26+
^0
27+
25| | } else {
28+
26| | }
29+
27| | }
30+
28| 1| Ok(())
31+
29| 1| }
32+
30| |}
33+
31| |
34+
32| |struct DisplayTest;
35+
33| |
36+
34| |impl std::fmt::Display for DisplayTest {
37+
35| 1| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
38+
36| 1| if false {
39+
37| | } else {
40+
38| 1| if false {
41+
39| | while true {}
42+
40| 1| }
43+
41| 1| write!(f, "error")?;
44+
^0
45+
42| | }
46+
43| 10| for i in 0..10 {
47+
44| 10| if false {
48+
45| | } else {
49+
46| 10| if false {
50+
47| | while true {}
51+
48| 10| }
52+
49| 10| write!(f, "error")?;
53+
^0
54+
50| | }
55+
51| | }
56+
52| 1| Ok(())
57+
53| 1| }
58+
54| |}
59+
55| |
60+
56| 1|fn main() {
61+
57| 1| let debug_test = DebugTest;
62+
58| 1| println!("{:?}", debug_test);
63+
59| 1| let display_test = DisplayTest;
64+
60| 1| println!("{}", display_test);
65+
61| 1|}
3866

0 commit comments

Comments
 (0)