This is an archive of the discontinued LLVM Phabricator instance.

[CoverageMapping] Handle gaps in counter IDs for source-based coverage
ClosedPublic

Authored by pirama on May 3 2021, 12:21 PM.

Details

Summary

For source-based coverage, the frontend sets the counter IDs and the
constraints of counter IDs is not defined. For e.g., the Rust frontend
until recently had a reserved counter #0
(https://github.com/rust-lang/rust/pull/83774). Rust coverage
instrumentation also creates counters on edges in addition to basic
blocks. Some functions may have more counters than regions.

This breaks an assumption in CoverageMapping.cpp where the number of
counters in a function is assumed to be bounded by the number of
regions:

Counts.assign(Record.MappingRegions.size(), 0);

This assumption causes CounterMappingContext::evaluate() to fail since
there are not enough counter values created in the above call to
Counts.assign. Consequently, some uncovered functions are not
reported in coverage reports.

This change walks a Function's CoverageMappingRecord to find the maximum
counter ID, and uses it to initialize the counter array when instrprof
records are missing for a function in sparse profiles.

Diff Detail

Event Timeline

pirama created this revision.May 3 2021, 12:21 PM
pirama requested review of this revision.May 3 2021, 12:21 PM
Herald added a project: Restricted Project. · View Herald TranscriptMay 3 2021, 12:21 PM
vsk added a comment.May 7 2021, 10:16 AM

Thanks for digging into this. Could you explain why it's necessary to use more coverage counters than there are regions? If there's no longer a reserved counter (counter #0), then are counters N >= Regions.size() somehow redundant (and if so, are they worth deduplicating in the Rust FE)?

Thanks for digging into this. Could you explain why it's necessary to use more coverage counters than there are regions? If there's no longer a reserved counter (counter #0), then are counters N >= Regions.size() somehow redundant (and if so, are they worth deduplicating in the Rust FE)?

I'm not very familiar with the Rust FE. Here's one example I see when we build this for Android:

Counter in file 0 520:20 -> 520:21, #1
Counter in file 0 520:24 -> 520:25, (#6 + ((((#5 + #4) + #3) + #2) + (#1 - (#6 + (#5 + (#4 + (#3 + #2)))))))

There are only two regions in this function which seems to be a generated function for a map.

ok_or(
    LOG_LEVEL_NAMES
        .iter()
        .position(|&name| eq_ignore_ascii_case(name, level))
        .into_iter()
        .filter(|&idx| idx != 0)
        .map(|idx| Level::from_usize(idx).unwrap())
        .next(),

Line 520 corresponds to the map call.

Adding @richkadel in case he has more details. IIUC, Rust FE adds counters before frontend DCE. The coverage mapping is written after DCE, so is skipped for any deleted regions. But the counter expressions may remain and refer to these deleted counters. Also, this example is from an older version of Rust FE. There may be new fixes in the Rust nightly builds but not sure if this pass ordering issue would still be present.

At a high level, we don't currently document this assumption or requirement for LLVM coverage and there's no error/warning when it is violated. So other frontends can make similar errors as well. Making llmv-cov accept this would prevent this assumption from tripping other FEs. An alternate to this change would be detect violations and have LLVM issue a warning/error.

vsk added a comment.EditedMay 11 2021, 9:26 AM

Thanks for the context. I haven't looked at the Rust FE's coverage implementation, so I'm not familiar with any tradeoffs inherent to the implementation approach. One thing you wrote does concern me:

Rust FE adds counters before frontend DCE. The coverage mapping is written after DCE, so is skipped for any deleted regions.

While it isn't a hard requirement that the source -> coverage mapping become immutable before optimization begins, that is the traditional approach taken by clang & swiftc. (We nod towards this in https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#impact-of-llvm-optimizations-on-coverage-reports, but you're right that the documentation could be improved.) IIUC Rust's behavior is a bit of a departure. Although it probably saves some binary size in *.prof{raw,data} files (I wonder how much?), it does also introduce additional complexity.

I see two options for moving forward:

  1. Have llvm be more permissive about the number of counters when loading sparse .profdata (this patch)
  2. Have llvm be less permissive (assert that #counters < #regions), document the new requirement and change the Rust FE to emit counters before DCE

I think (2) would lead to less complexity overall in both Rust & llvm, but both sound reasonable to me. Wdyt?

While it isn't a hard requirement that the source -> coverage mapping become immutable before optimization begins, that is the traditional approach taken by clang & swiftc. (We nod towards this in https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#impact-of-llvm-optimizations-on-coverage-reports,

@vsk - Just so I'm clear, are you saying that every Counter and Expression generated is guaranteed to be added to the coverage map (so there are no index gaps), even if the counter is part of an unreachable (and therefore eliminated) branch/block?

And, if so, I assume it's OK for a dead branch/block to be eliminated (so the instrprof.increment call for that branch's counter is never added.) But even if the code to increment that counter is not in the code, the coverage map includes the counter. Right?

I recently added support for reporting code regions for dead blocks, but I am replacing them with Zero counters. If I'm understanding you correctly, then I may be able to just retain the original counter id and/or expression and add them to the map instead of converting them to Zero counters, and therefore meet your expectations.

I see two options for moving forward:

  1. Have llvm be more permissive about the number of counters when loading sparse .profdata (this patch)
  2. Have llvm be less permissive (assert that #counters < #regions), document the new requirement and change the Rust FE to emit counters before DCE

I think (2) would lead to less complexity overall in both Rust & llvm, but both sound reasonable to me. Wdyt?

Unless I misunderstand the above, then yes, option 2. And the assertion error message needs to explain the problem (something like, "non-contiguous counter IDs are not allowed") if you discover a coverage map has counter ID gaps or expressions that refer to missing counters.

Thanks for pointing this out and let me know what I have wrong here.

@vsk - Also, I want to confirm that it is OK to have counters that are incremented (via instrprof.increment intrinsic) that are not associated with a code region.

Without a code region, I can't add that counter to the coverage map, so, assuming this is not violating LLVM's assumptions, I guess there may be counter ID gaps.

I'm not sure what clang or swiftc does, but in the Rust FE, I sometimes need to count edges between nodes in the CFG (branches taken, between two blocks) since the target block can be executed from multiple incoming edges.

This seems to work fine so far, but I want to make sure. And my suggested error message (prior comment) is probably not accurate (I now realize).

This stuff is hard to explain :-)

@vsk - Also, I want to confirm that it is OK to have counters that are incremented (via instrprof.increment intrinsic) that are not associated with a code region.

The expectation is that No. of code regions > Max. counter ID. If there are no gaps in counter IDs, and counters IDs start at 0, then Max. counter ID = No. of counters - 1, so we'd need No. of code regions >= No. of counters.

The underlying reasoning is that for line coverage, we can assign one counter per code region and so we'd need any more counters than regions.

Without a code region, I can't add that counter to the coverage map, so, assuming this is not violating LLVM's assumptions, I guess there may be counter ID gaps.

I'm not sure what clang or swiftc does, but in the Rust FE, I sometimes need to count edges between nodes in the CFG (branches taken, between two blocks) since the target block can be executed from multiple incoming edges.

Do we need counters on edges if we're only interested in line segment coverage?

This seems to work fine so far, but I want to make sure. And my suggested error message (prior comment) is probably not accurate (I now realize).

This stuff is hard to explain :-)

Do we need counters on edges if we're only interested in line segment coverage?

Edge counters exist so they can be operands in expressions, and since they don't have a code region, they would not be referenced directly by a code region in a report; but the expression might, and that's we we need them.

(I don't think "line coverage" is a factor. The coverage map only concerns itself with the regions. If the region happens to have one or more newlines, or if there are multiple regions on a line, that isn't relevant to the coverage map; at least not for Rust. llvm-cov computes line coverages when generating the report.)

pirama added a comment.EditedMay 11 2021, 3:16 PM

Do we need counters on edges if we're only interested in line segment coverage?

Edge counters exist so they can be operands in expressions, and since they don't have a code region, they would not be referenced directly by a code region in a report; but the expression might, and that's we we need them.

Edge counters help my understanding of the following example I had posted earlier. Here, counters #2-#6 are presumably edge counters.

Counter in file 0 520:20 -> 520:21, #1
Counter in file 0 520:24 -> 520:25, (#6 + ((((#5 + #4) + #3) + #2) + (#1 - (#6 + (#5 + (#4 + (#3 + #2)))))))

My question was based o the following strategy: use one counter per region and then attempt to reduce the number of counters using counter expressions. Does using edge counters result in fewer counters compared to the above strategy? It's entirely possible I'm missing some knowledge about Rust semantics that requires edge counters.

The above example, even if we account for your Rust FE fix to remove the reserved counter #0 breaks the assumption that No. of code regions >= No. of counters. So I feel like this patch would be needed to accommodate this scenario.

As an aside, the counter expression above would reduce to #1 but don't think LLVM or the FE can simplify this.

(I don't think "line coverage" is a factor. The coverage map only concerns itself with the regions. If the region happens to have one or more newlines, or if there are multiple regions on a line, that isn't relevant to the coverage map; at least not for Rust. llvm-cov computes line coverages when generating the report.)

I should have used "region coverage" instead of "line segment coverage" in my original question.

An "edge counter", as I'm calling it, is a counter (not an expression). It gets converted into a call to the instrprof.increment intrinsic (so there is executable code behind it), but there is no source code represented by that count.

I don't think this is unique to Rust, but I'm not sure how quickly I can come up with an example in code. From a CFG perspective:

  B
 / \
C   D

We can use an expression to compute the count for one of the branches:

D = B - C

But that assumes C is only reached by B.

Consider a loop. The block inside the loop is reached via 2 or more edges: 1) from above the loop; 2) from the bottom of the loop (the backedge); and maybe from one or more continue statements.

So you can have something like this:

A   B
 \ / \
  C   D

Now the previous expression won't work.

I could count D, and then compute:

C = A + (B - D)

But this isn't always possible or practical (from my experience).

But if I create a counter that counts the edge B->C, I can do this:

C = A + B->C
D = B - B->C

The B->C edge counter is an actual instrprof.increment counter in the LLVM IR, and it represents part of the control flow of the program, but there is no way to tie that to a code region, and it wouldn't be useful in a report. The counters/regions that USE the edge counter are what you want to know.

vsk added a subscriber: alanphipps.EditedMay 11 2021, 4:45 PM

Just so I'm clear, are you saying that every Counter and Expression generated is guaranteed to be added to the coverage map (so there are no index gaps), even if the counter is part of an unreachable (and therefore eliminated) branch/block?

Yes, that's traditionally how we've done it. For example, in clang, we walk the function AST, prepare all the requisite counters & expressions, and emit an immutable coverage mapping into the llvm IR all before optimization begins:

// CodeGenPGO::assignRegionCounters
mapRegionCounters(D);
if (CGM.getCodeGenOpts().CoverageMapping)
  emitCounterRegionMapping(D);

And, if so, I assume it's OK for a dead branch/block to be eliminated (so the instrprof.increment call for that branch's counter is never added.) But even if the code to increment that counter is not in the code, the coverage map includes the counter. Right?

Yes, exactly. So long as a single instrprof.increment call within a function survives optimization, the profile will contain a full set of region counts for the function. If no instrprof.increment calls survive optimization, llvm behaves as if each counter referenced by the function's coverage mapping is 0.

I recently added support for reporting code regions for dead blocks, but I am replacing them with Zero counters. If I'm understanding you correctly, then I may be able to just retain the original counter id and/or expression and add them to the map instead of converting them to Zero counters, and therefore meet your expectations.

That sounds right. The expectation is that either the profile will contain a 0 count for the dead region/block, or (if the profile record is missing) llvm will infer that the entire function is dead. But this doesn't require any changes to the coverage mapping itself.

And the assertion error message needs to explain the problem (something like, "non-contiguous counter IDs are not allowed")

Right, I might suggest assert(getMaxCounterID(...) < Record.MappingRegions.size() && "Didn't expect to see more regions-with-counters than regions").

Also, I want to confirm that it is OK to have counters that are incremented (via instrprof.increment intrinsic) that are not associated with a code region.

Ah, interesting! I think that is OK. However it does mean that the assert written above is broken, and that we'd want llvm to become more permissive (along the line of @pirama's patch).

(An aside: maybe this is only of historical interest, but I suspect it's somewhat of an accident that this works at all. As far as I know, it was always assumed that each counter was associated with a specific ::CodeRegion.)

I don't think dispensing with the assert is a big loss. The alternative would be to have the FE create a special CounterMappingRegion::EdgeRegion every time it creates a counter that otherwise wouldn't have a region; this would be wasteful unless the extra regions would actually be used.

I'm not sure what clang or swiftc does, but in the Rust FE, I sometimes need to count edges between nodes in the CFG (branches taken, between two blocks) since the target block can be executed from multiple incoming edges

Clang recently picked up branch coverage support via (among other format changes) CoverageMappingRegion::BranchRegion. @alanphipps is the expert on this. Prior to Alan's change, clang simply didn't support branch/edge coverage.

Edge counters exist so they can be operands in expressions, and since they don't have a code region, they would not be referenced directly by a code region in a report; but the expression might, and that's we we need them.

Thanks for sharing the example. For context, in clang, when there's a block of code that has multiple entry points, we either assign a fresh counter to that block or come up with a workable counter expression purely in terms of other block counts. That's not to say it's not useful or valid to use edge counters instead, though.

OK, great. Thanks for confirming this!

vsk added inline comments.May 12 2021, 9:23 AM
llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
196

I'm not sure how this case can arise. Would it be preferable to assert C.getExpressionID() < Expressions.size() instead?

llvm/unittests/ProfileData/CoverageMappingTest.cpp
805

A bit of a nit, but: based on our discussion it seems like we want to allow counters to not be associated with code regions. That's not necessarily the same as allowing non-contiguous counter ID's (although it's fine that this patch does that).

I think it'd be helpful for the comments / test case name to reflect the former (maybe something like 'non_code_region_counter'): that'd leave the door open to disallowing non-contiguous counter ID's down the line.

819

It would tighten the test up to check that the first FunctionRecord has a .CountedRegions field with the expected size.

pirama updated this revision to Diff 346000.May 17 2021, 3:09 PM
pirama edited the summary of this revision. (Show Details)

Tighten and reword test. Also fix lint suggestion.

Thanks for the detail about the edge counters @richkadel.

llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
196

I mirrorred CounterMappingContext::evaluate() above, which implies this case could happen. Not sure if an assert is needed here since evaluate() would report an error anyway.

pirama marked 2 inline comments as done.May 17 2021, 3:11 PM
vsk accepted this revision.May 19 2021, 9:56 AM

Thanks, lgtm.

This revision is now accepted and ready to land.May 19 2021, 9:56 AM
This revision was landed with ongoing or failed builds.May 19 2021, 10:46 AM
This revision was automatically updated to reflect the committed changes.