verilator/src/VlcTop.cpp

502 lines
18 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: verilator_coverage: top implementation
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
#define VL_MT_DISABLED_CODE_UNIT 1
#include "VlcTop.h"
#include "V3Error.h"
#include "V3Os.h"
#include "VlcOptions.h"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <map>
#include <sstream>
#include <string>
#include <vector>
//######################################################################
void VlcTop::readCoverage(const string& filename, bool nonfatal) {
UINFO(2, "readCoverage " << filename);
std::ifstream is{filename.c_str()};
if (!is) {
if (!nonfatal) v3fatal("Can't read coverage file: " << filename);
return;
}
// Testrun and computrons argument unsupported as yet
VlcTest* const testp = tests().newTest(filename, 0, 0);
while (!is.eof()) {
const string line = V3Os::getline(is);
// UINFO(9, " got " << line);
if (line[0] == 'C') {
string::size_type secspace = 3;
for (; secspace < line.length(); secspace++) {
if (line[secspace] == '\'' && line[secspace + 1] == ' ') break;
}
const string point = line.substr(3, secspace - 3);
if (!opt.isTypeMatch(point.c_str())) continue;
const uint64_t hits = std::atoll(line.c_str() + secspace + 1);
// UINFO(9, " point '" << point << "'" << " " << hits);
const uint64_t pointnum = points().findAddPoint(point, hits);
if (opt.rank()) { // Only if ranking - uses a lot of memory
if (hits >= VlcBuckets::sufficient()) {
points().pointNumber(pointnum).testsCoveringInc();
testp->buckets().addData(pointnum, hits);
}
}
}
}
}
void VlcTop::writeCoverage(const string& filename) {
UINFO(2, "writeCoverage " << filename);
std::ofstream os{filename.c_str()};
if (!os) {
v3fatal("Can't write file: " << filename);
return;
}
os << "# SystemC::Coverage-3\n";
for (const auto& i : m_points) {
const VlcPoint& point = m_points.pointNumber(i.second);
os << "C '" << point.name() << "' " << point.count() << '\n';
}
}
void VlcTop::writeInfo(const string& filename) {
UINFO(2, "writeInfo " << filename);
std::ofstream os{filename.c_str()};
if (!os) {
v3fatal("Can't write file: " << filename);
return;
}
annotateCalc();
// See 'man lcov' for format details
// TN:<trace_file_name>
// Source file:
// SF:<absolute_path_to_source_file>
// FN:<line_number_of_function_start>,<function_name>
// FNDA:<execution_count>,<function_name>
// FNF:<number_functions_found>
// FNH:<number_functions_hit>
// Branches:
// BRDA:<line_number>,<block_number>,<branch_number>,<taken_count_or_-_for_zero>
// BRF:<number_of_branches_found>
// BRH:<number_of_branches_hit>
// Line counts:
// DA:<line_number>,<execution_count>
// LF:<number_of_lines_found>
// LH:<number_of_lines_hit>
// Section ending:
// end_of_record
os << "TN:verilator_coverage\n";
for (auto& si : m_sources) {
VlcSource& source = si.second;
os << "SF:" << source.name() << '\n';
VlcSource::LinenoMap& lines = source.lines();
int branchesFound = 0;
int branchesHit = 0;
for (auto& li : lines) {
VlcSourceCount& sc = li.second;
os << "DA:" << sc.lineno() << "," << sc.maxCount() << "\n";
const int num_branches = sc.points().size();
if (num_branches == 1) continue;
branchesFound += num_branches;
int point_num = 0;
for (const auto& point : sc.points()) {
os << "BRDA:" << sc.lineno() << ",";
os << "0,";
os << point_num << ",";
os << point->count() << "\n";
branchesHit += opt.countOk(point->count());
++point_num;
}
}
os << "BRF:" << branchesFound << '\n';
os << "BRH:" << branchesHit << '\n';
os << "end_of_record\n";
}
}
//********************************************************************
struct CmpComputrons final {
bool operator()(const VlcTest* lhsp, const VlcTest* rhsp) const {
if (lhsp->computrons() != rhsp->computrons()) {
return lhsp->computrons() < rhsp->computrons();
}
return lhsp->bucketsCovered() > rhsp->bucketsCovered();
}
};
void VlcTop::rank() {
UINFO(2, "rank...");
uint64_t nextrank = 1;
// Sort by computrons, so fast tests get selected first
std::vector<VlcTest*> bytime;
for (const auto& testp : m_tests) {
if (testp->bucketsCovered()) { // else no points, so can't help us
bytime.push_back(testp);
}
}
sort(bytime.begin(), bytime.end(), CmpComputrons()); // Sort the vector
VlcBuckets remaining;
for (const auto& i : m_points) {
const VlcPoint* const pointp = &points().pointNumber(i.second);
// If any tests hit this point, then we'll need to cover it.
if (pointp->testsCovering()) remaining.addData(pointp->pointNum(), 1);
}
// Additional Greedy algorithm
// O(n^2) Ouch. Probably the thing to do is randomize the order of data
// then hierarchically solve a small subset of tests, and take resulting
// solution and move up to larger subset of tests. (Aka quick sort.)
while (true) {
if (debug() >= 9) {
UINFO_PREFIX("Left on iter" << nextrank << ": "); // LCOV_EXCL_LINE
remaining.dump(); // LCOV_EXCL_LINE
}
VlcTest* bestTestp = nullptr;
uint64_t bestRemain = 0;
for (const auto& testp : bytime) {
if (!testp->rank()) {
const uint64_t remain = testp->buckets().dataPopCount(remaining);
if (remain > bestRemain) {
bestTestp = testp;
bestRemain = remain;
}
}
}
if (VlcTest* const testp = bestTestp) {
testp->rank(nextrank++);
testp->rankPoints(bestRemain);
remaining.orData(bestTestp->buckets());
} else {
break; // No test covering more stuff found
}
}
}
void VlcTop::covergroup() {
UINFO(2, "covergroup...");
// Structs for accumulating report data
struct BinEntry {
std::string name;
std::string binType; // "ignore", "illegal", or "" (normal)
bool covered = false;
uint64_t count = 0;
};
struct CpEntry {
std::string name;
bool isCross = false;
std::vector<BinEntry> bins;
uint64_t normalTotal = 0;
uint64_t normalCovered = 0;
};
struct CgEntry {
std::string typeName;
std::string filename;
int lineno = 0;
std::vector<CpEntry> coverpoints;
std::map<std::string, size_t> cpIndex;
};
std::map<std::string, CgEntry> cgMap;
// Collect covergroup points from all loaded coverage data
for (const auto& nameNum : m_points) {
const VlcPoint& pt = m_points.pointNumber(nameNum.second);
if (pt.type() != "covergroup") continue;
const std::string page = pt.page();
// Page format: "v_covergroup/<cgTypeName>"
const std::string pagePrefix = "v_covergroup/";
if (page.size() <= pagePrefix.size()) continue;
const std::string cgTypeName = page.substr(pagePrefix.size());
// Parse hier: "<cg_type>.<cp_name>.<bin_name>"
const std::string hier = pt.hier();
const size_t dot1 = hier.find('.');
if (dot1 == std::string::npos) continue;
const size_t dot2 = hier.find('.', dot1 + 1);
if (dot2 == std::string::npos) continue;
const std::string cpName = hier.substr(dot1 + 1, dot2 - dot1 - 1);
const std::string binName = hier.substr(dot2 + 1);
auto& cg = cgMap[cgTypeName];
if (cg.typeName.empty()) {
cg.typeName = cgTypeName;
cg.filename = pt.filename();
cg.lineno = pt.lineno();
}
auto it = cg.cpIndex.find(cpName);
size_t cpIdx;
if (it == cg.cpIndex.end()) {
cpIdx = cg.coverpoints.size();
cg.cpIndex[cpName] = cpIdx;
CpEntry cp;
cp.name = cpName;
cp.isCross = pt.isCross();
cg.coverpoints.push_back(cp);
} else {
cpIdx = it->second;
}
BinEntry bin;
bin.name = binName;
bin.binType = pt.binType();
// Threshold: use per-bin thresh key (option.at_least) if present, else 1 (SV default)
const std::string threshStr = pt.thresh();
const uint64_t binThresh = threshStr.empty() ? 1 : std::stoull(threshStr);
bin.count = pt.count();
bin.covered = (bin.count >= binThresh);
cg.coverpoints[cpIdx].bins.push_back(bin);
if (bin.binType.empty()) {
++cg.coverpoints[cpIdx].normalTotal;
if (bin.covered) ++cg.coverpoints[cpIdx].normalCovered;
}
}
// Compute grand totals
uint64_t grandTotal = 0, grandCovered = 0, grandIgnored = 0, grandIllegal = 0;
for (const auto& cgPair : cgMap) {
for (const auto& cp : cgPair.second.coverpoints) {
grandTotal += cp.normalTotal;
grandCovered += cp.normalCovered;
for (const auto& bin : cp.bins) {
if (bin.binType == "ignore") ++grandIgnored;
else if (bin.binType == "illegal") ++grandIllegal;
}
}
}
// Format a percentage string "xx.xx"
const auto pctStr = [](uint64_t covered, uint64_t total) -> std::string {
std::ostringstream oss;
const double pct = (total == 0) ? 100.0 : (100.0 * covered / total);
oss << std::fixed << std::setprecision(2) << pct;
return oss.str();
};
const std::string divider(78, '-');
// Header and grand total
std::cout << "COVERGROUP COVERAGE REPORT\n";
std::cout << "==========================\n";
std::cout << "\n";
std::cout << "TOTAL: " << grandCovered << "/" << grandTotal
<< " bins covered (" << pctStr(grandCovered, grandTotal) << "%)\n";
if (grandIgnored || grandIllegal)
std::cout << " (" << grandIgnored << " ignored, " << grandIllegal << " illegal)\n";
// One section per covergroup type (map is sorted alphabetically)
for (const auto& cgPair : cgMap) {
const CgEntry& cg = cgPair.second;
uint64_t cgTotal = 0, cgCovered = 0;
for (const auto& cp : cg.coverpoints) {
cgTotal += cp.normalTotal;
cgCovered += cp.normalCovered;
}
std::cout << "\n" << divider << "\n";
std::cout << "Covergroup Type: " << cg.typeName
<< " [" << cg.filename << ":" << cg.lineno << "]\n";
std::cout << " Type Coverage: " << cgCovered << "/" << cgTotal
<< " bins (" << pctStr(cgCovered, cgTotal) << "%)\n";
for (const auto& cp : cg.coverpoints) {
std::cout << "\n";
std::cout << " " << (cp.isCross ? "Cross" : "Coverpoint") << ": " << cp.name << "\n";
std::cout << " Coverage: " << cp.normalCovered << "/" << cp.normalTotal
<< " bins (" << pctStr(cp.normalCovered, cp.normalTotal) << "%)\n";
std::cout << " Bins:\n";
// Align bin name column to max name length in this coverpoint
size_t maxNameLen = 0;
for (const auto& bin : cp.bins)
if (bin.name.size() > maxNameLen) maxNameLen = bin.name.size();
for (const auto& bin : cp.bins) {
const char* status;
if (bin.binType == "ignore") status = "IGNORE ";
else if (bin.binType == "illegal") status = "ILLEGAL";
else if (bin.covered) status = "COVERED";
else status = "ZERO ";
std::cout << " " << status << " "
<< std::left << std::setw(static_cast<int>(maxNameLen)) << bin.name
<< std::right << " " << bin.count << " hits\n";
}
}
}
std::cout << "\n" << divider << "\n";
}
//######################################################################
void VlcTop::annotateCalc() {
// Calculate per-line information into filedata structure
for (const auto& i : m_points) {
const VlcPoint& point = m_points.pointNumber(i.second);
const string filename = point.filename();
const int lineno = point.lineno();
if (!filename.empty() && lineno != 0) {
VlcSource& source = sources().findNewSource(filename);
UINFO(9, "AnnoCalc count " << filename << ":" << lineno << ":" << point.column() << " "
<< point.count() << " " << point.linescov());
// Base coverage
source.insertPoint(lineno, &point);
// Additional lines covered by this statement
bool range = false;
int start = 0;
int end = 0;
const string linescov = point.linescov();
for (const char* covp = linescov.c_str(); true; ++covp) {
if (!*covp || *covp == ',') { // Ending
for (int lni = start; start && lni <= end; ++lni) {
source.insertPoint(lni, &point);
}
if (!*covp) break;
start = 0; // Prep for next
end = 0;
range = false;
} else if (*covp == '-') {
range = true;
} else if (std::isdigit(*covp)) {
const char* const digitsp = covp;
while (std::isdigit(*covp)) ++covp;
--covp; // Will inc in for loop
if (!range) start = std::atoi(digitsp);
end = std::atoi(digitsp);
}
}
}
}
}
void VlcTop::annotateCalcNeeded() {
// Compute which files are needed. A file isn't needed if it has appropriate
// coverage in all categories
int totCases = 0;
int totOk = 0;
for (auto& si : m_sources) {
VlcSource& source = si.second;
// UINFO(1, "Source " << source.name());
if (opt.annotateAll()) source.needed(true);
const VlcSource::LinenoMap& lines = source.lines();
for (auto& li : lines) {
const VlcSourceCount& sc = li.second;
// UINFO(0, "Source " << source.name() << ":" << sc.lineno() << ":" << sc.column());
++totCases;
if (opt.countOk(sc.minCount())) {
++totOk;
} else {
source.needed(true);
}
}
}
const float pct = totCases ? (100 * totOk / totCases) : 0;
std::cout << "Total coverage (" << totOk << "/" << totCases << ") ";
std::cout << std::fixed << std::setw(3) << std::setprecision(2) << pct << "%\n";
if (totOk != totCases) cout << "See lines with '%00' in " << opt.annotateOut() << '\n';
}
void VlcTop::annotateOutputFiles(const string& dirname) {
// Create if uncreated, ignore errors
V3Os::createDir(dirname);
for (auto& si : m_sources) {
VlcSource& source = si.second;
if (!source.needed()) continue;
const string filename = source.name();
const string outfilename = dirname + "/" + V3Os::filenameNonDir(filename);
UINFO(1, "annotateOutputFile " << filename << " -> " << outfilename);
std::ifstream is{filename.c_str()};
if (!is) {
v3error("Can't read annotation file: " << filename);
return;
}
std::ofstream os{outfilename.c_str()};
if (!os) {
v3error("Can't write file: " << outfilename);
return;
}
os << "// // verilator_coverage annotation\n";
int lineno = 0;
while (!is.eof()) {
lineno++;
const std::string line = V3Os::getline(is);
VlcSource::LinenoMap& lines = source.lines();
const auto lit = lines.find(lineno);
if (lit == lines.end()) {
os << " " << line << '\n';
} else {
VlcSourceCount& sc = lit->second;
// UINFO(0, "Source " << source.name() << ":" << sc.lineno() << ":" <<
// sc.column());
const bool minOk = opt.countOk(sc.minCount());
const bool maxOk = opt.countOk(sc.maxCount());
if (minOk) {
os << " ";
} else if (maxOk) {
os << "~";
} else {
os << "%";
}
os << std::setfill('0') << std::setw(6) << sc.maxCount() << " " << line << '\n';
if (opt.annotatePoints()) {
for (const auto& pit : sc.points()) pit->dumpAnnotate(os, opt.annotateMin());
}
}
}
}
}
void VlcTop::annotate(const string& dirname) {
// Calculate per-line information into filedata structure
annotateCalc();
annotateCalcNeeded();
annotateOutputFiles(dirname);
}