Rollup merge of #122860 - Zalathar:unused, r=cjgillot

coverage: Re-enable `UnreachablePropagation` for coverage builds

This is a sequence of 3 related changes:
- Clean up the existing code that scans for unused functions
- Detect functions that were instrumented for coverage, but have had all their coverage statements removed by later MIR transforms (e.g. `UnreachablePropagation`)
- Re-enable `UnreachablePropagation` in coverage builds

Because we now detect functions that have lost their coverage statements, and treat them as unused, we don't need to worry about `UnreachablePropagation` removing all of those statements. This is demonstrated by `tests/coverage/unreachable.rs`.

Fixes #116171.
This commit is contained in:
Guillaume Gomez 2024-03-27 10:13:42 +01:00 committed by GitHub
commit 8a7f285cbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 95 additions and 79 deletions

View File

@ -6,9 +6,8 @@
use itertools::Itertools as _; use itertools::Itertools as _;
use rustc_codegen_ssa::traits::{BaseTypeMethods, ConstMethods}; use rustc_codegen_ssa::traits::{BaseTypeMethods, ConstMethods};
use rustc_data_structures::fx::{FxIndexMap, FxIndexSet}; use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
use rustc_hir::def::DefKind; use rustc_hir::def_id::{DefId, LocalDefId};
use rustc_hir::def_id::DefId;
use rustc_index::IndexVec; use rustc_index::IndexVec;
use rustc_middle::bug; use rustc_middle::bug;
use rustc_middle::mir; use rustc_middle::mir;
@ -335,16 +334,9 @@ fn save_function_record(
); );
} }
/// When finalizing the coverage map, `FunctionCoverage` only has the `CodeRegion`s and counters for /// Each CGU will normally only emit coverage metadata for the functions that it actually generates.
/// the functions that went through codegen; such as public functions and "used" functions /// But since we don't want unused functions to disappear from coverage reports, we also scan for
/// (functions referenced by other "used" or public items). Any other functions considered unused, /// functions that were instrumented but are not participating in codegen.
/// or "Unreachable", were still parsed and processed through the MIR stage, but were not
/// codegenned. (Note that `-Clink-dead-code` can force some unused code to be codegenned, but
/// that flag is known to cause other errors, when combined with `-C instrument-coverage`; and
/// `-Clink-dead-code` will not generate code for unused generic functions.)
///
/// We can find the unused functions (including generic functions) by the set difference of all MIR
/// `DefId`s (`tcx` query `mir_keys`) minus the codegenned `DefId`s (`codegenned_and_inlined_items`).
/// ///
/// These unused functions don't need to be codegenned, but we do need to add them to the function /// These unused functions don't need to be codegenned, but we do need to add them to the function
/// coverage map (in a single designated CGU) so that we still emit coverage mappings for them. /// coverage map (in a single designated CGU) so that we still emit coverage mappings for them.
@ -354,75 +346,109 @@ fn add_unused_functions(cx: &CodegenCx<'_, '_>) {
assert!(cx.codegen_unit.is_code_coverage_dead_code_cgu()); assert!(cx.codegen_unit.is_code_coverage_dead_code_cgu());
let tcx = cx.tcx; let tcx = cx.tcx;
let usage = prepare_usage_sets(tcx);
let eligible_def_ids = tcx.mir_keys(()).iter().filter_map(|local_def_id| { let is_unused_fn = |def_id: LocalDefId| -> bool {
let def_id = local_def_id.to_def_id(); let def_id = def_id.to_def_id();
let kind = tcx.def_kind(def_id);
// `mir_keys` will give us `DefId`s for all kinds of things, not // To be eligible for "unused function" mappings, a definition must:
// just "functions", like consts, statics, etc. Filter those out. // - Be function-like
if !matches!(kind, DefKind::Fn | DefKind::AssocFn | DefKind::Closure) { // - Not participate directly in codegen (or have lost all its coverage statements)
return None; // - Not have any coverage statements inlined into codegenned functions
} tcx.def_kind(def_id).is_fn_like()
&& (!usage.all_mono_items.contains(&def_id)
|| usage.missing_own_coverage.contains(&def_id))
&& !usage.used_via_inlining.contains(&def_id)
};
// Scan for unused functions that were instrumented for coverage.
for def_id in tcx.mir_keys(()).iter().copied().filter(|&def_id| is_unused_fn(def_id)) {
// Get the coverage info from MIR, skipping functions that were never instrumented.
let body = tcx.optimized_mir(def_id);
let Some(function_coverage_info) = body.function_coverage_info.as_deref() else { continue };
// FIXME(79651): Consider trying to filter out dummy instantiations of // FIXME(79651): Consider trying to filter out dummy instantiations of
// unused generic functions from library crates, because they can produce // unused generic functions from library crates, because they can produce
// "unused instantiation" in coverage reports even when they are actually // "unused instantiation" in coverage reports even when they are actually
// used by some downstream crate in the same binary. // used by some downstream crate in the same binary.
Some(local_def_id.to_def_id())
});
let codegenned_def_ids = codegenned_and_inlined_items(tcx);
// For each `DefId` that should have coverage instrumentation but wasn't
// codegenned, add it to the function coverage map as an unused function.
for def_id in eligible_def_ids.filter(|id| !codegenned_def_ids.contains(id)) {
// Skip any function that didn't have coverage data added to it by the
// coverage instrumentor.
let body = tcx.instance_mir(ty::InstanceDef::Item(def_id));
let Some(function_coverage_info) = body.function_coverage_info.as_deref() else {
continue;
};
debug!("generating unused fn: {def_id:?}"); debug!("generating unused fn: {def_id:?}");
let instance = declare_unused_fn(tcx, def_id); add_unused_function_coverage(cx, def_id, function_coverage_info);
add_unused_function_coverage(cx, instance, function_coverage_info);
} }
} }
/// All items participating in code generation together with (instrumented) struct UsageSets<'tcx> {
/// items inlined into them. all_mono_items: &'tcx DefIdSet,
fn codegenned_and_inlined_items(tcx: TyCtxt<'_>) -> DefIdSet { used_via_inlining: FxHashSet<DefId>,
let (items, cgus) = tcx.collect_and_partition_mono_items(()); missing_own_coverage: FxHashSet<DefId>,
let mut visited = DefIdSet::default(); }
let mut result = items.clone();
for cgu in cgus { /// Prepare sets of definitions that are relevant to deciding whether something
for item in cgu.items().keys() { /// is an "unused function" for coverage purposes.
if let mir::mono::MonoItem::Fn(ref instance) = item { fn prepare_usage_sets<'tcx>(tcx: TyCtxt<'tcx>) -> UsageSets<'tcx> {
let did = instance.def_id(); let (all_mono_items, cgus) = tcx.collect_and_partition_mono_items(());
if !visited.insert(did) {
continue; // Obtain a MIR body for each function participating in codegen, via an
} // arbitrary instance.
let body = tcx.instance_mir(instance.def); let mut def_ids_seen = FxHashSet::default();
for block in body.basic_blocks.iter() { let def_and_mir_for_all_mono_fns = cgus
for statement in &block.statements { .iter()
let mir::StatementKind::Coverage(_) = statement.kind else { continue }; .flat_map(|cgu| cgu.items().keys())
let scope = statement.source_info.scope; .filter_map(|item| match item {
if let Some(inlined) = scope.inlined_instance(&body.source_scopes) { mir::mono::MonoItem::Fn(instance) => Some(instance),
result.insert(inlined.def_id()); mir::mono::MonoItem::Static(_) | mir::mono::MonoItem::GlobalAsm(_) => None,
} })
} // We only need one arbitrary instance per definition.
} .filter(move |instance| def_ids_seen.insert(instance.def_id()))
.map(|instance| {
// We don't care about the instance, just its underlying MIR.
let body = tcx.instance_mir(instance.def);
(instance.def_id(), body)
});
// Functions whose coverage statments were found inlined into other functions.
let mut used_via_inlining = FxHashSet::default();
// Functions that were instrumented, but had all of their coverage statements
// removed by later MIR transforms (e.g. UnreachablePropagation).
let mut missing_own_coverage = FxHashSet::default();
for (def_id, body) in def_and_mir_for_all_mono_fns {
let mut saw_own_coverage = false;
// Inspect every coverage statement in the function's MIR.
for stmt in body
.basic_blocks
.iter()
.flat_map(|block| &block.statements)
.filter(|stmt| matches!(stmt.kind, mir::StatementKind::Coverage(_)))
{
if let Some(inlined) = stmt.source_info.scope.inlined_instance(&body.source_scopes) {
// This coverage statement was inlined from another function.
used_via_inlining.insert(inlined.def_id());
} else {
// Non-inlined coverage statements belong to the enclosing function.
saw_own_coverage = true;
} }
} }
if !saw_own_coverage && body.function_coverage_info.is_some() {
missing_own_coverage.insert(def_id);
}
} }
result UsageSets { all_mono_items, used_via_inlining, missing_own_coverage }
} }
fn declare_unused_fn<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> ty::Instance<'tcx> { fn add_unused_function_coverage<'tcx>(
ty::Instance::new( cx: &CodegenCx<'_, 'tcx>,
def_id: LocalDefId,
function_coverage_info: &'tcx mir::coverage::FunctionCoverageInfo,
) {
let tcx = cx.tcx;
let def_id = def_id.to_def_id();
// Make a dummy instance that fills in all generics with placeholders.
let instance = ty::Instance::new(
def_id, def_id,
ty::GenericArgs::for_item(tcx, def_id, |param, _| { ty::GenericArgs::for_item(tcx, def_id, |param, _| {
if let ty::GenericParamDefKind::Lifetime = param.kind { if let ty::GenericParamDefKind::Lifetime = param.kind {
@ -431,14 +457,8 @@ fn declare_unused_fn<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> ty::Instance<'tc
tcx.mk_param_from_def(param) tcx.mk_param_from_def(param)
} }
}), }),
) );
}
fn add_unused_function_coverage<'tcx>(
cx: &CodegenCx<'_, 'tcx>,
instance: ty::Instance<'tcx>,
function_coverage_info: &'tcx mir::coverage::FunctionCoverageInfo,
) {
// An unused function's mappings will automatically be rewritten to map to // An unused function's mappings will automatically be rewritten to map to
// zero, because none of its counters/expressions are marked as seen. // zero, because none of its counters/expressions are marked as seen.
let function_coverage = FunctionCoverageCollector::unused(instance, function_coverage_info); let function_coverage = FunctionCoverageCollector::unused(instance, function_coverage_info);

View File

@ -14,11 +14,7 @@
impl MirPass<'_> for UnreachablePropagation { impl MirPass<'_> for UnreachablePropagation {
fn is_enabled(&self, sess: &rustc_session::Session) -> bool { fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
// Enable only under -Zmir-opt-level=2 as this can make programs less debuggable. // Enable only under -Zmir-opt-level=2 as this can make programs less debuggable.
sess.mir_opt_level() >= 2
// FIXME(#116171) Coverage gets confused by MIR passes that can remove all
// coverage statements from an instrumented function. This pass can be
// re-enabled when coverage codegen is robust against that happening.
sess.mir_opt_level() >= 2 && !sess.instrument_coverage()
} }
fn run_pass<'tcx>(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { fn run_pass<'tcx>(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {

View File

@ -14,11 +14,11 @@ Number of expressions: 0
Number of file 0 mappings: 1 Number of file 0 mappings: 1
- Code(Counter(0)) at (prev + 17, 1) to (start + 1, 37) - Code(Counter(0)) at (prev + 17, 1) to (start + 1, 37)
Function name: unreachable::unreachable_intrinsic Function name: unreachable::unreachable_intrinsic (unused)
Raw bytes (9): 0x[01, 01, 00, 01, 01, 16, 01, 01, 2c] Raw bytes (9): 0x[01, 01, 00, 01, 00, 16, 01, 01, 2c]
Number of files: 1 Number of files: 1
- file 0 => global file 1 - file 0 => global file 1
Number of expressions: 0 Number of expressions: 0
Number of file 0 mappings: 1 Number of file 0 mappings: 1
- Code(Counter(0)) at (prev + 22, 1) to (start + 1, 44) - Code(Zero) at (prev + 22, 1) to (start + 1, 44)