Hi, at the moment I am trying to generate a code coverage report which should combine the coverage of multiple test executables, i.e. I have a static library A and e.g. two executables linking agains that library and executing tests. What I do today is:
Building the library and tests with -fprofile-instr-generate and -fcoverage-mapping (and also -fno-inline -fno-exceptions).
Then running both test executables TestA and TestB with environment variable LLVM_PROFILE_FILE set to different files.
Then combine the profraw files created using llvm-profdata merge -o merge.profdata …(all profraw files from above).
That gives me a single profdata file I now want to process using llvm-cov command. I try to run llvm-cov show --instr-profile=merge.profdata -object TestA TestB.
That generates a coverage report and looks also good overall. However, I run into an issue for a specific functions (actually in a header file) which are not being covered in the report, although there are tests calling the function. The very interesting thing here is, in case I reorder the llvm-cov call with -object TestB TestA it shows coverage as expected.
Now my question: Is this a use-case that should work? Can you explain why the order of the executables in the llvm-cov makes a difference (and should it be like this?). Do you have any advice about a correct approach?
I will try to produce a minimal example of this behavior.
I also tried to debug a little bit the llvm-cov tool and found inside CoverageMapping.cpp two interesting places. The following if becomes true and thus the proper coverage of the function is skipped when the test1 is the second one loaded.
However, I would have expected the if shoto hit for the test2 where the function is actually unused. But Counts[0] is actually == 0 (while the other two conditions are true).
The 0 in the Counts comes from the instrprof_error::unknown_function error above which sets 0 to Counts.
In a few words, you need to specify --object for each individual object file you’re passing to llvm-cov: llvm-cov show --instr-profile=merge.profdata --object TestA --object TestB
I would call this a design flaw of llvm-cov.
A warm suggestion is to use llvm-cov only to export data to LCOV tracefile format and then continue all processing and report generation using lcov. llvm-cov cannot reliably do merging of data from multiple sources.
thank you very much for your feedback. Are you sure using --object multiple times solves the issue? I wondered that it solves it for me too, but then I checked the source code of llvm-cov and had no explanation why. Using --dump-collected-objects gave me the hint:
–object TestA --object TestB results in TestA processed first, then TestB
–object TestA TestB switches the order, so it is equivalent to --object TestB --object TestA
So I think the order here is what fixed the issue for me (just due to the internal processing in llvm-cov where I am not proficient enough right now to judge on it. But I think there is no “correct” order, because it depends on the function we look at what’s the correct order.
Isn’t the actual merging happening in llvm-profdata where merge the two profraw to a single profdata file?
Hi @Aleksa_Markovic , I also tried experimenting with merging the exportet lcov data but that also have a major flow.
In case a method is not generated (since in a header and not called) the lcov files contain a “0 hits” entry for each line of the method, even for lines which do not contain any executable code. However, when I look into the coverage when the method is called, these empty lines are not asssociated to coverage.
When now merging, we cannot decide what to do with these lines, we would need to apply some kind of magic to only take lines which are in all lcovs, but that would again eliminate totally uncovered function.
So I think this entire thing cannot be workarounded at the moment
But it also shows an error “Unexecuted instantiation: _ZNK4test4Test9getToTestEv” with a subview of the function body.
Another thing I noticed is that the main functions in in test1.exe and test2.exe show execution count 2 instead of 1, which is incorrect. My guess is that it double counts main function in both binaries:
Okay, I also had some further look today to try to understand what’s going on and I think we have some more issues. Let me try to summarize my observation:
When the method in the header file is not called, it is not emitted by the compiler. However, the coverage information in the binary still contains the function with a hash of zero. If we now consider two test test executables, of which on calls the method and the other one not, execute both and merge the profraw files into a single profdata, we get the following situation:
test1: does call the method and the coverage information contains the “correct” hash != 0
test2: does not call the method and the coverage information contains the hash == 0
profdata: contains counters from the execution of test1 with the “correct” hash
The llvm-cov tool now reads the two test binaries in the order as provided on the commandline. It now sees the not-emitted function with hash zero and stores it internally (and assuming all counters to be zero, see llvm-project/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp at dcf0160bd61d150e7b94067fcd991b466a361b08 · llvm/llvm-project · GitHub). When later the acutally emitted function with hash != 0 is processed, it is ignored because the implementation thinks that the method is already processed. If we now change the order of the binaries on the command line, by accident, the one which has the hash != 0 is processed first, resulting in the expected report.
Great analysis. Maybe coverage should treat functions with hash being 0 specially such that they can be merged to the functions with the same names but have non-zero hash.
I tried to change the condition from Counts[0] > 0 to Counts[0] == 0. It makes the example you provided to work but failed a test in compiler-rt/test/profile/instrprof-merging.cpp (specifically line 20). I’ll take a look on that a bit more.
Your guess is correct and it’s due to main() having the exact same symbol name and hash:
$ llvm-profdata show test1.profraw --all-functions
Counters:
main:
Hash: 0x0000000000000018
Counters: 1
Function count: 1
# ...
$ llvm-profdata show test2.profraw --all-functions
Counters:
main:
Hash: 0x0000000000000018
Counters: 1
Function count: 1
Even though this is a minimal example, I can imagine there will be cases where small, branch-less functions merge like this even though they shouldn’t. We should consider changing the way hashes are generated.
Cc: @chapuni
To further clarify my statement on unreliable merging:
Yes, but the merge criteria is a matching hash and symbol name. There is no information about the source file, as this mapping exists only in the object file containing the symbols. In ZequanWu’s analysis, we see that two different main()s merge into one because their hashes and names matched.
After better understanding what you’ve presented here, I think the only way to get consistent results is:
not to do any merging, just convert data from a single profraw to a single profdata
similarly, do not pass more than one object file to llvm-cov. You can perform matching of which profraw originate from which object file using build IDs (llvm-profdata show --binary-ids , introduce with clang++ -Wl,--build-id)
export the data to LCOV tracefile format and continue all manipulations with lcov.
This is all from experience moving from gcov-compatible coverage to Source-based for a 5 MLOC codebase. I really wish the tools Source-based have these issues resolved, as it’s very hard to get trustworthy and consistent results with so many bugs and workarounds.