// -*- 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 #include #include #include #include #include #include //###################################################################### 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: // Source file: // SF: // FN:, // FNDA:, // FNF: // FNH: // Branches: // BRDA:,,, // BRF: // BRH: // Line counts: // DA:, // LF: // LH: // 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 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 bins; uint64_t normalTotal = 0; uint64_t normalCovered = 0; }; struct CgEntry { std::string typeName; std::string filename; int lineno = 0; std::vector coverpoints; std::map cpIndex; }; std::map 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/" const std::string pagePrefix = "v_covergroup/"; if (page.size() <= pagePrefix.size()) continue; const std::string cgTypeName = page.substr(pagePrefix.size()); // Parse hier: ".." 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(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); }