-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
coverage bug fixes and optimization support #83307
Conversation
Important bug fixes here. I know I have to resolve conflicts after @tmiasko 's change, but I wanted to push the validated changes I made in parallel. I'll merge in the recent changes, and then we'll have support for both I haven't yet tested this on Mac or Windows. Depending on how quickly this can be reviewed, it would be helpful to do a And lastly, I may need to update the documentation, via separate PR. |
20f0dad
to
a71c080
Compare
This comment has been minimized.
This comment has been minimized.
a71c080
to
2eab02a
Compare
This comment has been minimized.
This comment has been minimized.
2eab02a
to
171a334
Compare
Adjusted LLVM codegen for code compiled with `-Zinstrument-coverage` to address multiple, somewhat related issues. Fixed a significant flaw in prior coverage solution: Every counter generated a new counter variable, but there should have only been one counter variable per function. This appears to have bloated .profraw files significantly. (For a small program, it increased the size by about 40%. I have not tested large programs, but there is anecdotal evidence that profraw files were way too large. This is a good fix, regardless, but hopefully it also addresses related issues. Fixes: rust-lang#82144 Invalid LLVM coverage data produced when compiled with -C opt-level=1 Existing tests now work up to at least `opt-level=3`. This required a detailed analysis of the LLVM IR, comparisons with Clang C++ LLVM IR when compiled with coverage, and a lot of trial and error with codegen adjustments. The biggest hurdle was figuring out how to continue to support coverage results for unused functions and generics. Rust's coverage results have three advantages over Clang's coverage results: 1. Rust's coverage map does not include any overlapping code regions, making coverage counting unambiguous. 2. Rust generates coverage results (showing zero counts) for all unused functions, including generics. (Clang does not generate coverage for uninstantiated template functions.) 3. Rust's unused functions produce minimal stubbed functions in LLVM IR, sufficient for including in the coverage results; while Clang must generate the complete LLVM IR for each unused function, even though it will never be called. This PR removes the previous hack of attempting to inject coverage into some other existing function instance, and generates dedicated instances for each unused function. This change, and a few other adjustments (similar to what is required for `-C link-dead-code`, but with lower impact), makes it possible to support LLVM optimizations. Fixes: rust-lang#79651 Coverage report: "Unexecuted instantiation:..." for a generic function from multiple crates Fixed by removing the aforementioned hack. Some "Unexecuted instantiation" notices are unavoidable, as explained in the `used_crate.rs` test, but `-Zinstrument-coverage` has new options to back off support for either unused generics, or all unused functions, which avoids the notice, at the cost of less coverage of unused functions. Fixes: rust-lang#82875 Invalid LLVM coverage data produced with crate brotli_decompressor Fixed by disabling the LLVM function attribute that forces inlining, if `-Z instrument-coverage` is enabled. This attribute is applied to Rust functions with `#[inline(always)], and in some cases, the forced inlining breaks coverage instrumentation and reports.
171a334
to
bcf7555
Compare
That's surprising; nice find! |
@bors try |
⌛ Trying commit bcf7555 with merge 47994a37de9e400e74ae445fb4b7adcaaa11dbf4... |
This comment has been minimized.
This comment has been minimized.
(UPDATE: WeakAnyLinkage appears to resolve this problem.) FYI, below are the linker errors. This isn't totally surprising, based on my solution to injecting synthetic unused functions (so that they aren't removed by downstream optimizations). I'm not sure how to create a sufficient test for this, but I expect/hope to solve this by tweaking the linkage/visibility settings, in unsafe {
llvm::LLVMRustSetLinkage(llfn, llvm::Linkage::ExternalLinkage);
llvm::LLVMRustSetVisibility(llfn, llvm::Visibility::Hidden);
} This is just a snippet of the linker errors. There are 100s of these errors. Note, this is only using $ cd json5format
$ RUSTC=$HOME/rust/build/x86_64-unknown-linux-gnu/stage1/bin/rustc \
RUSTFLAGS="-Zinstrument-coverage" \
RUSTDOCFLAGS="-Zinstrument-coverage -Zunstable-options --persist-doctests target/debug/doctestbins" \
LLVM_PROFILE_FILE="json5format-%m.profraw" \
cargo test
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't get too far yet, but excited for this change :)
// Add a new `FunctionCoverage` to the `function_coverage_map`, with unreachable code regions | ||
// for each region in it's MIR. | ||
|
||
// Convert the `HashSet` of `codegenned_def_ids` to a sortable vector, and sort them. | ||
let mut sorted_codegenned_def_ids: Vec<DefId> = | ||
codegenned_def_ids.iter().map(|def_id| *def_id).collect(); | ||
sorted_codegenned_def_ids.sort_unstable(); | ||
|
||
let mut first_covered_def_id_by_file: FxHashMap<Symbol, DefId> = FxHashMap::default(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this with fresh eyes (maybe we discussed this before).. This will only work if there is a covered def in each file containing an uncovered def.
I'm wondering if we can do it a different way by sorting the Symbol
(which is just an index IIRC), partitioning by the number of codegen units, and using the index of the current codegen unit to determine which files to generate uncovered symbols for. In fact this could use the DefIds directly instead of Symbols.
Of course, that assumes there's a way to get an index of the current codegen unit and the number of codegen units.. not sure if there is or if we can add one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's better than it used to be. Previously the file had to have at least one non-generic instance. Now it's fine to have only generics.
I thought about this a lot this week. Yes, I agree, I'd love to be able to do this differently, but I don't understand enough about the CGU structure to visualize a better solution.
Can we keep this good, but challenging (for me), suggestion for a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, as an experiment, I added a library crate called unused_crate.rs
, and added one pub const
, then referenced only the const in the uses_crate.rs
test. If the unused_crate
defines any public non-generic functions, at least one of them will be eagerly generated into an Instance, and that's enough to have coverage for all of them (even private or generic). But if the crate ONLY has private non-generic and/or public generics, nothing gets coverage. So that's the only case.
HOWEVER! Even when they do get coverage in the coverage map (because there is one public non-generic), llvm-cov show
STILL doesn't generate a coverage report for that file for whatever reason.
Maybe that's a bug, or just the way it works, but it looks like even if we change the way mapgen.rs works, it will apparently have no net effect or improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One clarification, if any function in the crate actually is used, then you should get coverage (including in reports). My previous comment is only talking about limitations with reporting unused coverage in a crate that has no used functions.
☀️ Try build successful - checks-actions |
@richkadel I think you could declare the symbols as weak, since their definitions should all be the same. That'll get rid of the multiple definition errors. |
I'll give that a try. Thanks! |
The sample json5format tests produce coverage results again (and work with opt-level 3!)
Good news:
I think this addresses all of the known issues in this PR, but @tmandry still plans to continue reviewing. @wesleywiser - You may be interested in at least skimming these changes if you have time. Any feedback is appreciated. Thanks! |
@tmandry - I made all of your suggested changes, and replied to your comment expressing concern about inlining across CGUs. I'm not sure that I can do much, or if it really is a problem, but the change I added was required (tests break without it). If you have a test case that we can use to show this doesn't work, or other ideas, let me know. I haven't seen a problem from this yet though (including compiling with |
@@ -2876,14 +2876,9 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, id: DefId) -> CodegenFnAttrs { | |||
InlineAttr::None | |||
} else if list_contains_name(&items[..], sym::always) { | |||
if tcx.sess.instrument_coverage() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this again, I'd prefer if this fix isn't in typeck. From a compiler architecture standpoint, that means coverage flags can change language-level semantics of the program.
Looking at all the references to InlineAttr
there are a handful of places in the backend where it gets used. Maybe a helper method would be good here. I'd also accept a FIXME to address this later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually a FIXME might be preferred, since this is much simpler. I'm not 100% sure what the right answer is here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll add the FIXME. I searched a variety of locations, but this one was the most central that I could find.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added #83429
@bors delegate+ rollup=never r=me with comments addressed, or happy to take another look if you like. |
✌️ @richkadel can now approve this pull request |
de24b34
to
0859cec
Compare
@bors r=tmandry rollup=never |
📌 Commit 0859cec has been approved by |
@bors retry |
⌛ Testing commit 0859cec with merge 9ebafb70ed5d3f150e2026c71dbe07ab548b6e18... |
The job Click to see the possible cause of the failure (guessed by this bot)
|
💔 Test failed - checks-actions |
|
@bors retry |
I finished looking through the rest of this and this is really great work @richkadel! |
☀️ Test successful - checks-actions |
Adjusted LLVM codegen for code compiled with
-Zinstrument-coverage
toaddress multiple, somewhat related issues.
Fixed a significant flaw in prior coverage solution: Every counter
generated a new counter variable, but there should have only been one
counter variable per function. This appears to have bloated .profraw
files significantly. (For a small program, it increased the size by
about 40%. I have not tested large programs, but there is anecdotal
evidence that profraw files were way too large. This is a good fix,
regardless, but hopefully it also addresses related issues.
Fixes: #82144
Invalid LLVM coverage data produced when compiled with -C opt-level=1
Existing tests now work up to at least
opt-level=3
. This required adetailed analysis of the LLVM IR, comparisons with Clang C++ LLVM IR
when compiled with coverage, and a lot of trial and error with codegen
adjustments.
The biggest hurdle was figuring out how to continue to support coverage
results for unused functions and generics. Rust's coverage results have
three advantages over Clang's coverage results:
making coverage counting unambiguous.
functions, including generics. (Clang does not generate coverage for
uninstantiated template functions.)
sufficient for including in the coverage results; while Clang must
generate the complete LLVM IR for each unused function, even though
it will never be called.
This PR removes the previous hack of attempting to inject coverage into
some other existing function instance, and generates dedicated instances
for each unused function. This change, and a few other adjustments
(similar to what is required for
-C link-dead-code
, but with lowerimpact), makes it possible to support LLVM optimizations.
Fixes: #79651
Coverage report: "Unexecuted instantiation:..." for a generic function
from multiple crates
Fixed by removing the aforementioned hack. Some "Unexecuted
instantiation" notices are unavoidable, as explained in the
used_crate.rs
test, but-Zinstrument-coverage
has new options toback off support for either unused generics, or all unused functions,
which avoids the notice, at the cost of less coverage of unused
functions.
Fixes: #82875
Invalid LLVM coverage data produced with crate brotli_decompressor
Fixed by disabling the LLVM function attribute that forces inlining, if
-Z instrument-coverage
is enabled. This attribute is applied toRust functions with `#[inline(always)], and in some cases, the forced
inlining breaks coverage instrumentation and reports.
FYI: @wesleywiser
r? @tmandry