[llvm] bea39c5 - [llvm-cov] Support directory layout in coverage reports

Yuhao Gu via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 23 22:46:34 PDT 2023


Author: Yuhao Gu
Date: 2023-08-24T13:46:12+08:00
New Revision: bea39c5443612b638aa1cc56d36e3b1fd06f6e96

URL: https://github.com/llvm/llvm-project/commit/bea39c5443612b638aa1cc56d36e3b1fd06f6e96
DIFF: https://github.com/llvm/llvm-project/commit/bea39c5443612b638aa1cc56d36e3b1fd06f6e96.diff

LOG: [llvm-cov] Support directory layout in coverage reports

This is a GSoC 2023 project ([discourse link](https://discourse.llvm.org/t/coverage-support-a-hierarchical-directory-structure-in-generated-coverage-html-reports/68239)).

llvm-cov currently generates a single top-level index HTML file, which causes rendering scalability issues in large projects. This patch adds support for hierarchical directory structure into the HTML reports to solve scalability issues by introducing the following changes:

- Added a new command line option `--show-directory-coverage` for `llvm-cov show`. It works both for `--format=html` and `--format=text`.
- Two new classes: `CoveragePrinterHTMLDirectory` and `CoveragePrinterTextDirectory` was added to support the new option.
- A tool class `DirectoryCoverageReport` was added to support the two classes above.
- Updated the document.
- Added a new regression test for `--show-directory-coverage`.

Reviewed By: phosek, gulfem

Differential Revision: https://reviews.llvm.org/D151283

Added: 
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1.h
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_1.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_2.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.covmapping
    llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.profdata
    llvm/test/tools/llvm-cov/directory_coverage.linux.test
    llvm/test/tools/llvm-cov/directory_coverage.win.test

Modified: 
    llvm/docs/CommandGuide/llvm-cov.rst
    llvm/tools/llvm-cov/CodeCoverage.cpp
    llvm/tools/llvm-cov/CoverageReport.cpp
    llvm/tools/llvm-cov/CoverageReport.h
    llvm/tools/llvm-cov/CoverageSummaryInfo.h
    llvm/tools/llvm-cov/CoverageViewOptions.h
    llvm/tools/llvm-cov/SourceCoverageView.cpp
    llvm/tools/llvm-cov/SourceCoverageView.h
    llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
    llvm/tools/llvm-cov/SourceCoverageViewHTML.h
    llvm/tools/llvm-cov/SourceCoverageViewText.cpp
    llvm/tools/llvm-cov/SourceCoverageViewText.h
    llvm/tools/llvm-cov/TestingSupport.cpp

Removed: 
    


################################################################################
diff  --git a/llvm/docs/CommandGuide/llvm-cov.rst b/llvm/docs/CommandGuide/llvm-cov.rst
index 792f9629dbfbd0..ea70e170642eb2 100644
--- a/llvm/docs/CommandGuide/llvm-cov.rst
+++ b/llvm/docs/CommandGuide/llvm-cov.rst
@@ -249,6 +249,11 @@ OPTIONS
  line, but show the individual regions if there are multiple on the line.
  Defaults to false.
 
+.. option:: -show-directory-coverage
+
+ Generate an index file in each directory that contains at least one source
+ file with a top level index showing aggregates. Defaults to false.
+
 .. option:: -use-color
 
  Enable or disable color output. By default this is autodetected.

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc
new file mode 100644
index 00000000000000..8ff5d210e7810f
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/a0/a1/a2.cc
@@ -0,0 +1,7 @@
+#include "../../header.h"
+
+template<>
+bool equal<int>(int a, int b)
+{
+  return a == b;
+}

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1.h
new file mode 100644
index 00000000000000..32b872ac9960b5
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1.h
@@ -0,0 +1,3 @@
+int add(int a, int b);
+
+int sub(int a, int b);

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_1.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_1.cc
new file mode 100644
index 00000000000000..b824723dc84945
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_1.cc
@@ -0,0 +1,5 @@
+#include "b1.h"
+
+int add(int a, int b) {
+  return a + b;
+}

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_2.cc
new file mode 100644
index 00000000000000..5038dedcc6eb21
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/b0/b1_2.cc
@@ -0,0 +1,5 @@
+#include "b1.h"
+
+int sub(int a, int b) {
+  return a - b;
+}

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h
new file mode 100644
index 00000000000000..483705194b3e97
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2.h
@@ -0,0 +1,9 @@
+int mul(int a, int b);
+
+int div(int a, int b);
+
+#ifdef DEF
+int div(int a, int b) {
+  return a / b;
+}
+#endif

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc
new file mode 100644
index 00000000000000..5557720d7c0db1
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_1.cc
@@ -0,0 +1,3 @@
+int mul(int a, int b) {
+  return a * b;
+}

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc
new file mode 100644
index 00000000000000..82ba2cc359c767
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/c0/c1/c2_2.cc
@@ -0,0 +1,2 @@
+#define DEF
+#include "c2.h"

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h
new file mode 100644
index 00000000000000..6a4eb2d60723a2
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/header.h
@@ -0,0 +1,5 @@
+#include "b0/b1.h"
+#include "c0/c1/c2.h"
+
+template<typename T>
+bool equal(T a, T b);

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc
new file mode 100644
index 00000000000000..10d403f085bc19
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.cc
@@ -0,0 +1,9 @@
+#include "header.h"
+
+int main() {
+  int a = 3;
+  int b = 4;
+  int c = c = add(a, b);
+  int d = mul(a, b);
+  return equal(a, sub(c, b)) - equal(a, div(d, b));
+}

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.covmapping b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.covmapping
new file mode 100644
index 00000000000000..58f7aadcc76a1e
Binary files /dev/null and b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.covmapping 
diff er

diff  --git a/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.profdata b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.profdata
new file mode 100644
index 00000000000000..decd124de4ee66
Binary files /dev/null and b/llvm/test/tools/llvm-cov/Inputs/directory_coverage/main.profdata 
diff er

diff  --git a/llvm/test/tools/llvm-cov/directory_coverage.linux.test b/llvm/test/tools/llvm-cov/directory_coverage.linux.test
new file mode 100644
index 00000000000000..5db76c5ab833f4
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/directory_coverage.linux.test
@@ -0,0 +1,53 @@
+# REQUIRES: system-linux
+# RUN: mkdir -p %t
+
+# RUN: llvm-cov show %S/Inputs/directory_coverage/main.covmapping \
+# RUN:   --instr-profile %S/Inputs/directory_coverage/main.profdata \
+# RUN:   --path-equivalence=/tmp/directory_coverage,%S/Inputs/directory_coverage \
+# RUN:   --format=text --show-directory-coverage -o %t/report-text
+
+# RUN: llvm-cov show %S/Inputs/directory_coverage/main.covmapping \
+# RUN:   --instr-profile %S/Inputs/directory_coverage/main.profdata \
+# RUN:   --path-equivalence=/tmp/directory_coverage,%S/Inputs/directory_coverage \
+# RUN:   --format=html --show-directory-coverage -o %t/report-html
+
+# RUN: FileCheck --input-file %t/report-text/index.txt %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/index.txt %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/b0/index.txt %s --check-prefix=B0
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/c0/c1/index.txt %s --check-prefix=C1
+
+# RUN: FileCheck --input-file %t/report-html/index.html %s --check-prefix=HTML-TOP --allow-empty
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/index.html %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/b0/index.html %s --check-prefix=B0
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/c0/c1/index.html %s --check-prefix=C1
+
+
+
+# HTML-TOP: coverage/index.html
+
+# ROOT: a0/a1/a2.cc
+# ROOT: b0/
+# ROOT-NOT: b1_1.cc
+# ROOT-NOT: b1_2.cc
+# ROOT: c0/c1/
+# ROOT-NOT: c2_1.cc
+# ROOT-NOT: b2_2.cc
+# ROOT: main.cc
+
+# B0: b1_1.cc
+# B0: b1_2.cc
+
+# C1: c2.h
+# C1: c2_1.cc
+
+
+For regenerating the test:
+
+cp -r %S/Inputs/directory_coverage /tmp
+cd /tmp/directory_coverage
+clang -fprofile-instr-generate -fcoverage-mapping -mllvm -enable-name-compression=false \
+  -o main main.cc a0/a1/a2.cc b0/b1_1.cc b0/b1_2.cc c0/c1/c2_1.cc c0/c1/c2_2.cc
+./main
+llvm-cov convert-for-testing main -o main.covmapping
+llvm-profdata merge default.profraw -o main.profdata
+rm main default.profraw

diff  --git a/llvm/test/tools/llvm-cov/directory_coverage.win.test b/llvm/test/tools/llvm-cov/directory_coverage.win.test
new file mode 100644
index 00000000000000..f948bdcae3a58b
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/directory_coverage.win.test
@@ -0,0 +1,44 @@
+# REQUIRES: system-windows
+# RUN: mkdir -p %t
+
+# RUN: llvm-cov show %S/Inputs/directory_coverage/main.covmapping \
+# RUN:   --instr-profile %S/Inputs/directory_coverage/main.profdata \
+# RUN:   --path-equivalence=/tmp/directory_coverage,%S/Inputs/directory_coverage \
+# RUN:   --format=text --show-directory-coverage -o %t/report-text
+
+# RUN: llvm-cov show %S/Inputs/directory_coverage/main.covmapping \
+# RUN:   --instr-profile %S/Inputs/directory_coverage/main.profdata \
+# RUN:   --path-equivalence=/tmp/directory_coverage,%S/Inputs/directory_coverage \
+# RUN:   --format=html --show-directory-coverage -o %t/report-html
+
+# RUN: FileCheck --input-file %t/report-text/index.txt %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/index.txt %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/b0/index.txt %s --check-prefix=B0
+# RUN: FileCheck --input-file %t/report-text/coverage/tmp/directory_coverage/c0/c1/index.txt %s --check-prefix=C1
+
+# RUN: FileCheck --input-file %t/report-html/index.html %s --check-prefix=HTML-TOP --allow-empty
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/index.html %s --check-prefix=ROOT
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/b0/index.html %s --check-prefix=B0
+# RUN: FileCheck --input-file %t/report-html/coverage/tmp/directory_coverage/c0/c1/index.html %s --check-prefix=C1
+
+
+
+# HTML-TOP: coverage\index.html
+
+# ROOT: a0\a1\a2.cc
+# ROOT: b0\
+# ROOT-NOT: b1_1.cc
+# ROOT-NOT: b1_2.cc
+# ROOT: c0\c1\
+# ROOT-NOT: c2_1.cc
+# ROOT-NOT: b2_2.cc
+# ROOT: main.cc
+
+# B0: b1_1.cc
+# B0: b1_2.cc
+
+# C1: c2.h
+# C1: c2_1.cc
+
+
+The input of this test is generated on Linux. See 'directory_coverage.linux.test'.

diff  --git a/llvm/tools/llvm-cov/CodeCoverage.cpp b/llvm/tools/llvm-cov/CodeCoverage.cpp
index 3992dfa8932c35..f26db153d4e369 100644
--- a/llvm/tools/llvm-cov/CodeCoverage.cpp
+++ b/llvm/tools/llvm-cov/CodeCoverage.cpp
@@ -982,6 +982,10 @@ int CodeCoverageTool::doShow(int argc, const char **argv,
                                    cl::desc("Show function instantiations"),
                                    cl::init(true), cl::cat(ViewCategory));
 
+  cl::opt<bool> ShowDirectoryCoverage("show-directory-coverage", cl::Optional,
+                                      cl::desc("Show directory coverage"),
+                                      cl::cat(ViewCategory));
+
   cl::opt<std::string> ShowOutputDirectory(
       "output-dir", cl::init(""),
       cl::desc("Directory in which coverage information is written out"));
@@ -1062,6 +1066,7 @@ int CodeCoverageTool::doShow(int argc, const char **argv,
   ViewOpts.ShowBranchPercents =
       ShowBranches == CoverageViewOptions::BranchOutputType::Percent;
   ViewOpts.ShowFunctionInstantiations = ShowInstantiations;
+  ViewOpts.ShowDirectoryCoverage = ShowDirectoryCoverage;
   ViewOpts.ShowOutputDirectory = ShowOutputDirectory;
   ViewOpts.TabSize = TabSize;
   ViewOpts.ProjectTitle = ProjectTitle;

diff  --git a/llvm/tools/llvm-cov/CoverageReport.cpp b/llvm/tools/llvm-cov/CoverageReport.cpp
index cb0b184e103c1c..518f6cd3efa2f4 100644
--- a/llvm/tools/llvm-cov/CoverageReport.cpp
+++ b/llvm/tools/llvm-cov/CoverageReport.cpp
@@ -166,6 +166,35 @@ unsigned getRedundantPrefixLen(ArrayRef<std::string> Paths) {
   return PrefixLen;
 }
 
+/// Determine the length of the longest redundant prefix of the substrs starts
+/// from \p LCP in \p Paths. \p Paths can't be empty. If there's only one
+/// element in \p Paths, the length of the substr is returned. Note this is
+/// 
diff ernet from the behavior of the function above.
+unsigned getRedundantPrefixLen(ArrayRef<StringRef> Paths, unsigned LCP) {
+  assert(!Paths.empty() && "Paths must have at least one element");
+
+  auto Iter = Paths.begin();
+  auto IterE = Paths.end();
+  auto Prefix = Iter->substr(LCP);
+  while (++Iter != IterE) {
+    auto Other = Iter->substr(LCP);
+    auto Len = std::min(Prefix.size(), Other.size());
+    for (std::size_t I = 0; I < Len; ++I) {
+      if (Prefix[I] != Other[I]) {
+        Prefix = Prefix.substr(0, I);
+        break;
+      }
+    }
+  }
+
+  for (auto I = Prefix.size(); --I != SIZE_MAX;) {
+    if (Prefix[I] == '/' || Prefix[I] == '\\')
+      return I + 1;
+  }
+
+  return Prefix.size();
+}
+
 } // end anonymous namespace
 
 namespace llvm {
@@ -180,8 +209,14 @@ void CoverageReport::render(const FileCoverageSummary &File,
       determineCoveragePercentageColor(File.InstantiationCoverage);
   auto LineCoverageColor = determineCoveragePercentageColor(File.LineCoverage);
   SmallString<256> FileName = File.Name;
-  sys::path::remove_dots(FileName, /*remove_dot_dot=*/true);
   sys::path::native(FileName);
+
+  // remove_dots will remove trailing slash, so we need to check before it.
+  auto IsDir = FileName.endswith(sys::path::get_separator());
+  sys::path::remove_dots(FileName, /*remove_dot_dot=*/true);
+  if (IsDir)
+    FileName += sys::path::get_separator();
+
   OS << column(FileName, FileReportColumns[0], Column::NoTrim);
 
   if (Options.ShowRegionSummary) {
@@ -436,7 +471,12 @@ void CoverageReport::renderFileReports(
   FileCoverageSummary Totals("TOTAL");
   auto FileReports =
       prepareFileReports(Coverage, Totals, Files, Options, Filters);
+  renderFileReports(OS, FileReports, Totals, Filters.empty());
+}
 
+void CoverageReport::renderFileReports(
+    raw_ostream &OS, const std::vector<FileCoverageSummary> &FileReports,
+    const FileCoverageSummary &Totals, bool ShowEmptyFiles) const {
   std::vector<StringRef> Filenames;
   Filenames.reserve(FileReports.size());
   for (const FileCoverageSummary &FCS : FileReports)
@@ -467,21 +507,20 @@ void CoverageReport::renderFileReports(
   renderDivider(FileReportColumns, OS);
   OS << "\n";
 
-  bool EmptyFiles = false;
+  std::vector<const FileCoverageSummary *> EmptyFiles;
   for (const FileCoverageSummary &FCS : FileReports) {
     if (FCS.FunctionCoverage.getNumFunctions())
       render(FCS, OS);
     else
-      EmptyFiles = true;
+      EmptyFiles.push_back(&FCS);
   }
 
-  if (EmptyFiles && Filters.empty()) {
+  if (!EmptyFiles.empty() && ShowEmptyFiles) {
     OS << "\n"
        << "Files which contain no functions:\n";
 
-    for (const FileCoverageSummary &FCS : FileReports)
-      if (!FCS.FunctionCoverage.getNumFunctions())
-        render(FCS, OS);
+    for (auto FCS : EmptyFiles)
+      render(*FCS, OS);
   }
 
   renderDivider(FileReportColumns, OS);
@@ -489,4 +528,96 @@ void CoverageReport::renderFileReports(
   render(Totals, OS);
 }
 
+Expected<FileCoverageSummary> DirectoryCoverageReport::prepareDirectoryReports(
+    ArrayRef<std::string> SourceFiles) {
+  std::vector<StringRef> Files(SourceFiles.begin(), SourceFiles.end());
+
+  unsigned RootLCP = getRedundantPrefixLen(Files, 0);
+  auto LCPath = Files.front().substr(0, RootLCP);
+
+  ThreadPoolStrategy PoolS = hardware_concurrency(Options.NumThreads);
+  if (Options.NumThreads == 0) {
+    PoolS = heavyweight_hardware_concurrency(Files.size());
+    PoolS.Limit = true;
+  }
+  ThreadPool Pool(PoolS);
+
+  TPool = &Pool;
+  LCPStack = {RootLCP};
+  FileCoverageSummary RootTotals(LCPath);
+  if (auto E = prepareSubDirectoryReports(Files, &RootTotals))
+    return {std::move(E)};
+  return {std::move(RootTotals)};
+}
+
+/// Filter out files in LCPStack.back(), group others by subdirectory name
+/// and recurse on them. After returning from all subdirectories, call
+/// generateSubDirectoryReport(). \p Files must be non-empty. The
+/// FileCoverageSummary of this directory will be added to \p Totals.
+Error DirectoryCoverageReport::prepareSubDirectoryReports(
+    const ArrayRef<StringRef> &Files, FileCoverageSummary *Totals) {
+  assert(!Files.empty() && "Files must have at least one element");
+
+  auto LCP = LCPStack.back();
+  auto LCPath = Files.front().substr(0, LCP).str();
+
+  // Use ordered map to keep entries in order.
+  SubFileReports SubFiles;
+  SubDirReports SubDirs;
+  for (auto &&File : Files) {
+    auto SubPath = File.substr(LCPath.size());
+    SmallVector<char, 128> NativeSubPath;
+    sys::path::native(SubPath, NativeSubPath);
+    StringRef NativeSubPathRef(NativeSubPath.data(), NativeSubPath.size());
+
+    auto I = sys::path::begin(NativeSubPathRef);
+    auto E = sys::path::end(NativeSubPathRef);
+    assert(I != E && "Such case should have been filtered out in the caller");
+
+    auto Name = SubPath.substr(0, I->size());
+    if (++I == E) {
+      auto Iter = SubFiles.insert_or_assign(Name, SubPath).first;
+      // Makes files reporting overlap with subdir reporting.
+      TPool->async(&CoverageReport::prepareSingleFileReport, File, &Coverage,
+                   Options, LCP, &Iter->second, &Filters);
+    } else {
+      SubDirs[Name].second.push_back(File);
+    }
+  }
+
+  // Call recursively on subdirectories.
+  for (auto &&KV : SubDirs) {
+    auto &V = KV.second;
+    if (V.second.size() == 1) {
+      // If there's only one file in that subdirectory, we don't bother to
+      // recurse on it further.
+      V.first.Name = V.second.front().substr(LCP);
+      TPool->async(&CoverageReport::prepareSingleFileReport, V.second.front(),
+                   &Coverage, Options, LCP, &V.first, &Filters);
+    } else {
+      auto SubDirLCP = getRedundantPrefixLen(V.second, LCP);
+      V.first.Name = V.second.front().substr(LCP, SubDirLCP);
+      LCPStack.push_back(LCP + SubDirLCP);
+      if (auto E = prepareSubDirectoryReports(V.second, &V.first))
+        return E;
+    }
+  }
+
+  TPool->wait();
+
+  FileCoverageSummary CurrentTotals(LCPath);
+  for (auto &&KV : SubFiles)
+    CurrentTotals += KV.second;
+  for (auto &&KV : SubDirs)
+    CurrentTotals += KV.second.first;
+  *Totals += CurrentTotals;
+
+  if (auto E = generateSubDirectoryReport(
+          std::move(SubFiles), std::move(SubDirs), std::move(CurrentTotals)))
+    return E;
+
+  LCPStack.pop_back();
+  return Error::success();
+}
+
 } // end namespace llvm

diff  --git a/llvm/tools/llvm-cov/CoverageReport.h b/llvm/tools/llvm-cov/CoverageReport.h
index f9a092f510b504..60f751ca967528 100644
--- a/llvm/tools/llvm-cov/CoverageReport.h
+++ b/llvm/tools/llvm-cov/CoverageReport.h
@@ -16,9 +16,12 @@
 #include "CoverageFilters.h"
 #include "CoverageSummaryInfo.h"
 #include "CoverageViewOptions.h"
+#include <map>
 
 namespace llvm {
 
+class ThreadPool;
+
 /// Displays the code coverage report.
 class CoverageReport {
   const CoverageViewOptions &Options;
@@ -62,6 +65,73 @@ class CoverageReport {
   /// in \p Filters.
   void renderFileReports(raw_ostream &OS, ArrayRef<std::string> Files,
                          const CoverageFiltersMatchAll &Filters) const;
+
+  /// Render file reports with given data.
+  void renderFileReports(raw_ostream &OS,
+                         const std::vector<FileCoverageSummary> &FileReports,
+                         const FileCoverageSummary &Totals,
+                         bool ShowEmptyFiles) const;
+};
+
+/// Prepare reports for every non-trivial directories (which have more than 1
+/// source files) of the source files. This class uses template method pattern.
+class DirectoryCoverageReport {
+public:
+  DirectoryCoverageReport(
+      const CoverageViewOptions &Options,
+      const coverage::CoverageMapping &Coverage,
+      const CoverageFiltersMatchAll &Filters = CoverageFiltersMatchAll())
+      : Options(Options), Coverage(Coverage), Filters(Filters) {}
+
+  virtual ~DirectoryCoverageReport() = default;
+
+  /// Prepare file reports for each directory in \p SourceFiles. The total
+  /// report for all files is returned and its Name is set to the LCP of all
+  /// files. The size of \p SourceFiles must be greater than 1 or else the
+  /// behavior is undefined, in which case you should use
+  /// CoverageReport::prepareSingleFileReport instead. If an error occurs,
+  /// the recursion will stop immediately.
+  Expected<FileCoverageSummary>
+  prepareDirectoryReports(ArrayRef<std::string> SourceFiles);
+
+protected:
+  // These member variables below are used for avoiding being passed
+  // repeatedly in recursion.
+  const CoverageViewOptions &Options;
+  const coverage::CoverageMapping &Coverage;
+  const CoverageFiltersMatchAll &Filters;
+
+  /// For calling CoverageReport::prepareSingleFileReport asynchronously
+  /// in prepareSubDirectoryReports(). It's not intended to be modified by
+  /// generateSubDirectoryReport().
+  ThreadPool *TPool;
+
+  /// One report level may correspond to multiple directory levels as we omit
+  /// directories which have only one subentry. So we use this Stack to track
+  /// each report level's corresponding drectory level.
+  /// Each value in the stack is the LCP prefix length length of that report
+  /// level. LCPStack.front() is the root LCP. Current LCP is LCPStack.back().
+  SmallVector<unsigned, 32> LCPStack;
+
+  // Use std::map to sort table rows in order.
+  using SubFileReports = std::map<StringRef, FileCoverageSummary>;
+  using SubDirReports =
+      std::map<StringRef,
+               std::pair<FileCoverageSummary, SmallVector<StringRef, 0>>>;
+
+  /// This method is called when a report level is prepared during the
+  /// recursion. \p SubFiles are the reports for those files directly in the
+  /// current directory. \p SubDirs are the reports for subdirectories in
+  /// current directory. \p SubTotals is the sum of all, and its name is the
+  /// current LCP. Note that this method won't be called for trivial
+  /// directories.
+  virtual Error generateSubDirectoryReport(SubFileReports &&SubFiles,
+                                           SubDirReports &&SubDirs,
+                                           FileCoverageSummary &&SubTotals) = 0;
+
+private:
+  Error prepareSubDirectoryReports(const ArrayRef<StringRef> &Files,
+                                   FileCoverageSummary *Totals);
 };
 
 } // end namespace llvm

diff  --git a/llvm/tools/llvm-cov/CoverageSummaryInfo.h b/llvm/tools/llvm-cov/CoverageSummaryInfo.h
index 84a3228f22b9a0..46510dc9cf3faf 100644
--- a/llvm/tools/llvm-cov/CoverageSummaryInfo.h
+++ b/llvm/tools/llvm-cov/CoverageSummaryInfo.h
@@ -222,6 +222,7 @@ struct FileCoverageSummary {
   FunctionCoverageInfo FunctionCoverage;
   FunctionCoverageInfo InstantiationCoverage;
 
+  FileCoverageSummary() = default;
   FileCoverageSummary(StringRef Name) : Name(Name) {}
 
   FileCoverageSummary &operator+=(const FileCoverageSummary &RHS) {

diff  --git a/llvm/tools/llvm-cov/CoverageViewOptions.h b/llvm/tools/llvm-cov/CoverageViewOptions.h
index fedf2df9b9bd76..eb852859e9cbe4 100644
--- a/llvm/tools/llvm-cov/CoverageViewOptions.h
+++ b/llvm/tools/llvm-cov/CoverageViewOptions.h
@@ -38,6 +38,7 @@ struct CoverageViewOptions {
   bool ShowBranchSummary;
   bool ShowRegionSummary;
   bool ShowInstantiationSummary;
+  bool ShowDirectoryCoverage;
   bool ExportSummaryOnly;
   bool SkipExpansions;
   bool SkipFunctions;

diff  --git a/llvm/tools/llvm-cov/SourceCoverageView.cpp b/llvm/tools/llvm-cov/SourceCoverageView.cpp
index ea86acadf0014e..7480b7f628a0b9 100644
--- a/llvm/tools/llvm-cov/SourceCoverageView.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageView.cpp
@@ -76,8 +76,12 @@ std::unique_ptr<CoveragePrinter>
 CoveragePrinter::create(const CoverageViewOptions &Opts) {
   switch (Opts.Format) {
   case CoverageViewOptions::OutputFormat::Text:
+    if (Opts.ShowDirectoryCoverage)
+      return std::make_unique<CoveragePrinterTextDirectory>(Opts);
     return std::make_unique<CoveragePrinterText>(Opts);
   case CoverageViewOptions::OutputFormat::HTML:
+    if (Opts.ShowDirectoryCoverage)
+      return std::make_unique<CoveragePrinterHTMLDirectory>(Opts);
     return std::make_unique<CoveragePrinterHTML>(Opts);
   case CoverageViewOptions::OutputFormat::Lcov:
     // Unreachable because CodeCoverage.cpp should terminate with an error

diff  --git a/llvm/tools/llvm-cov/SourceCoverageView.h b/llvm/tools/llvm-cov/SourceCoverageView.h
index 5a9fcdd1576182..c07595f980727a 100644
--- a/llvm/tools/llvm-cov/SourceCoverageView.h
+++ b/llvm/tools/llvm-cov/SourceCoverageView.h
@@ -99,7 +99,7 @@ class CoveragePrinter {
   CoveragePrinter(const CoverageViewOptions &Opts) : Opts(Opts) {}
 
   /// Return `OutputDir/ToplevelDir/Path.Extension`. If \p InToplevel is
-  /// false, skip the ToplevelDir component. If \p Relative is false, skip the
+  /// true, skip the ToplevelDir component. If \p Relative is true, skip the
   /// OutputDir component.
   std::string getOutputPath(StringRef Path, StringRef Extension,
                             bool InToplevel, bool Relative = true) const;

diff  --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
index 49f203507da0d7..79a0494815c2b6 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp
@@ -10,12 +10,13 @@
 ///
 //===----------------------------------------------------------------------===//
 
-#include "CoverageReport.h"
 #include "SourceCoverageViewHTML.h"
+#include "CoverageReport.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/Format.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/ThreadPool.h"
 #include <optional>
 
 using namespace llvm;
@@ -49,23 +50,41 @@ std::string escape(StringRef Str, const CoverageViewOptions &Opts) {
 }
 
 // Create a \p Name tag around \p Str, and optionally set its \p ClassName.
-std::string tag(const std::string &Name, const std::string &Str,
-                const std::string &ClassName = "") {
-  std::string Tag = "<" + Name;
-  if (!ClassName.empty())
-    Tag += " class='" + ClassName + "'";
-  return Tag + ">" + Str + "</" + Name + ">";
+std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") {
+  std::string Tag = "<";
+  Tag += Name;
+  if (!ClassName.empty()) {
+    Tag += " class='";
+    Tag += ClassName;
+    Tag += "'";
+  }
+  Tag += ">";
+  Tag += Str;
+  Tag += "</";
+  Tag += Name;
+  Tag += ">";
+  return Tag;
 }
 
 // Create an anchor to \p Link with the label \p Str.
-std::string a(const std::string &Link, const std::string &Str,
-              const std::string &TargetName = "") {
-  std::string Name = TargetName.empty() ? "" : ("name='" + TargetName + "' ");
-  return "<a " + Name + "href='" + Link + "'>" + Str + "</a>";
+std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") {
+  std::string Tag;
+  Tag += "<a ";
+  if (!TargetName.empty()) {
+    Tag += "name='";
+    Tag += TargetName;
+    Tag += "' ";
+  }
+  Tag += "href='";
+  Tag += Link;
+  Tag += "'>";
+  Tag += Str;
+  Tag += "</a>";
+  return Tag;
 }
 
 const char *BeginHeader =
-  "<head>"
+    "<head>"
     "<meta name='viewport' content='width=device-width,initial-scale=1'>"
     "<meta charset='UTF-8'>";
 
@@ -272,6 +291,57 @@ void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
   OS << EndHeader << "<body>";
 }
 
+void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts,
+                  const std::string &FirstCol, const FileCoverageSummary &FCS,
+                  bool IsTotals) {
+  SmallVector<std::string, 8> Columns;
+
+  // Format a coverage triple and add the result to the list of columns.
+  auto AddCoverageTripleToColumn =
+      [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) {
+        std::string S;
+        {
+          raw_string_ostream RSO{S};
+          if (Total)
+            RSO << format("%*.2f", 7, Pctg) << "% ";
+          else
+            RSO << "- ";
+          RSO << '(' << Hit << '/' << Total << ')';
+        }
+        const char *CellClass = "column-entry-yellow";
+        if (Pctg >= Opts.HighCovWatermark)
+          CellClass = "column-entry-green";
+        else if (Pctg < Opts.LowCovWatermark)
+          CellClass = "column-entry-red";
+        Columns.emplace_back(tag("td", tag("pre", S), CellClass));
+      };
+
+  Columns.emplace_back(tag("td", tag("pre", FirstCol)));
+  AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
+                            FCS.FunctionCoverage.getNumFunctions(),
+                            FCS.FunctionCoverage.getPercentCovered());
+  if (Opts.ShowInstantiationSummary)
+    AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
+                              FCS.InstantiationCoverage.getNumFunctions(),
+                              FCS.InstantiationCoverage.getPercentCovered());
+  AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
+                            FCS.LineCoverage.getNumLines(),
+                            FCS.LineCoverage.getPercentCovered());
+  if (Opts.ShowRegionSummary)
+    AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
+                              FCS.RegionCoverage.getNumRegions(),
+                              FCS.RegionCoverage.getPercentCovered());
+  if (Opts.ShowBranchSummary)
+    AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
+                              FCS.BranchCoverage.getNumBranches(),
+                              FCS.BranchCoverage.getPercentCovered());
+
+  if (IsTotals)
+    OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");
+  else
+    OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");
+}
+
 void emitEpilog(raw_ostream &OS) {
   OS << "</body>"
      << "</html>";
@@ -330,33 +400,44 @@ CoveragePrinterHTML::buildLinkToFile(StringRef SF,
   return a(LinkTarget, LinkText);
 }
 
+Error CoveragePrinterHTML::emitStyleSheet() {
+  auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
+  if (Error E = CSSOrErr.takeError())
+    return E;
+
+  OwnedStream CSS = std::move(CSSOrErr.get());
+  CSS->operator<<(CSSForCoverage);
+
+  return Error::success();
+}
+
+void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
+                                           const std::string &Title) {
+  // Emit some basic information about the coverage report.
+  if (Opts.hasProjectTitle())
+    OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));
+  OSRef << tag(ReportTitleTag, Title);
+  if (Opts.hasCreatedTime())
+    OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));
+
+  // Emit a link to some documentation.
+  OSRef << tag("p", "Click " +
+                        a("http://clang.llvm.org/docs/"
+                          "SourceBasedCodeCoverage.html#interpreting-reports",
+                          "here") +
+                        " for information about interpreting this report.");
+
+  // Emit a table containing links to reports for each file in the covmapping.
+  // Exclude files which don't contain any regions.
+  OSRef << BeginCenteredDiv << BeginTable;
+  emitColumnLabelsForIndex(OSRef, Opts);
+}
+
 /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
 /// false, link the summary to \p SF.
 void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
                                           const FileCoverageSummary &FCS,
                                           bool IsTotals) const {
-  SmallVector<std::string, 8> Columns;
-
-  // Format a coverage triple and add the result to the list of columns.
-  auto AddCoverageTripleToColumn =
-      [&Columns, this](unsigned Hit, unsigned Total, float Pctg) {
-        std::string S;
-        {
-          raw_string_ostream RSO{S};
-          if (Total)
-            RSO << format("%*.2f", 7, Pctg) << "% ";
-          else
-            RSO << "- ";
-          RSO << '(' << Hit << '/' << Total << ')';
-        }
-        const char *CellClass = "column-entry-yellow";
-        if (Pctg >= Opts.HighCovWatermark)
-          CellClass = "column-entry-green";
-        else if (Pctg < Opts.LowCovWatermark)
-          CellClass = "column-entry-red";
-        Columns.emplace_back(tag("td", tag("pre", S), CellClass));
-      };
-
   // Simplify the display file path, and wrap it in a link if requested.
   std::string Filename;
   if (IsTotals) {
@@ -365,43 +446,16 @@ void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
     Filename = buildLinkToFile(SF, FCS);
   }
 
-  Columns.emplace_back(tag("td", tag("pre", Filename)));
-  AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
-                            FCS.FunctionCoverage.getNumFunctions(),
-                            FCS.FunctionCoverage.getPercentCovered());
-  if (Opts.ShowInstantiationSummary)
-    AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
-                              FCS.InstantiationCoverage.getNumFunctions(),
-                              FCS.InstantiationCoverage.getPercentCovered());
-  AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
-                            FCS.LineCoverage.getNumLines(),
-                            FCS.LineCoverage.getPercentCovered());
-  if (Opts.ShowRegionSummary)
-    AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
-                              FCS.RegionCoverage.getNumRegions(),
-                              FCS.RegionCoverage.getPercentCovered());
-  if (Opts.ShowBranchSummary)
-    AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
-                              FCS.BranchCoverage.getNumBranches(),
-                              FCS.BranchCoverage.getPercentCovered());
-
-  if (IsTotals)
-    OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");
-  else
-    OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");
+  emitTableRow(OS, Opts, Filename, FCS, IsTotals);
 }
 
 Error CoveragePrinterHTML::createIndexFile(
     ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
     const CoverageFiltersMatchAll &Filters) {
   // Emit the default stylesheet.
-  auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
-  if (Error E = CSSOrErr.takeError())
+  if (Error E = emitStyleSheet())
     return E;
 
-  OwnedStream CSS = std::move(CSSOrErr.get());
-  CSS->operator<<(CSSForCoverage);
-
   // Emit a file index along with some coverage statistics.
   auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
   if (Error E = OSOrErr.takeError())
@@ -412,24 +466,8 @@ Error CoveragePrinterHTML::createIndexFile(
   assert(Opts.hasOutputDirectory() && "No output directory for index file");
   emitPrelude(OSRef, Opts, getPathToStyle(""));
 
-  // Emit some basic information about the coverage report.
-  if (Opts.hasProjectTitle())
-    OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));
-  OSRef << tag(ReportTitleTag, "Coverage Report");
-  if (Opts.hasCreatedTime())
-    OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));
-
-  // Emit a link to some documentation.
-  OSRef << tag("p", "Click " +
-                        a("http://clang.llvm.org/docs/"
-                          "SourceBasedCodeCoverage.html#interpreting-reports",
-                          "here") +
-                        " for information about interpreting this report.");
+  emitReportHeader(OSRef, "Coverage Report");
 
-  // Emit a table containing links to reports for each file in the covmapping.
-  // Exclude files which don't contain any regions.
-  OSRef << BeginCenteredDiv << BeginTable;
-  emitColumnLabelsForIndex(OSRef, Opts);
   FileCoverageSummary Totals("TOTALS");
   auto FileReports = CoverageReport::prepareFileReports(
       Coverage, Totals, SourceFiles, Opts, Filters);
@@ -465,6 +503,199 @@ Error CoveragePrinterHTML::createIndexFile(
   return Error::success();
 }
 
+struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
+  CoveragePrinterHTMLDirectory &Printer;
+
+  Reporter(CoveragePrinterHTMLDirectory &Printer,
+           const coverage::CoverageMapping &Coverage,
+           const CoverageFiltersMatchAll &Filters)
+      : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
+        Printer(Printer) {}
+
+  Error generateSubDirectoryReport(SubFileReports &&SubFiles,
+                                   SubDirReports &&SubDirs,
+                                   FileCoverageSummary &&SubTotals) override {
+    auto &LCPath = SubTotals.Name;
+    assert(Options.hasOutputDirectory() &&
+           "No output directory for index file");
+
+    SmallString<128> OSPath = LCPath;
+    sys::path::append(OSPath, "index");
+    auto OSOrErr = Printer.createOutputStream(OSPath, "html",
+                                              /*InToplevel=*/false);
+    if (auto E = OSOrErr.takeError())
+      return E;
+    auto OS = std::move(OSOrErr.get());
+    raw_ostream &OSRef = *OS.get();
+
+    auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
+                                               /*InToplevel=*/false);
+    emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath));
+
+    auto NavLink = buildTitleLinks(LCPath);
+    Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
+
+    std::vector<const FileCoverageSummary *> EmptyFiles;
+
+    // Make directories at the top of the table.
+    for (auto &&SubDir : SubDirs) {
+      auto &Report = SubDir.second.first;
+      if (!Report.FunctionCoverage.getNumFunctions())
+        EmptyFiles.push_back(&Report);
+      else
+        emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
+                     /*IsTotals=*/false);
+    }
+
+    for (auto &&SubFile : SubFiles) {
+      auto &Report = SubFile.second;
+      if (!Report.FunctionCoverage.getNumFunctions())
+        EmptyFiles.push_back(&Report);
+      else
+        emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
+                     /*IsTotals=*/false);
+    }
+
+    // Emit the totals row.
+    emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);
+    OSRef << EndTable << EndCenteredDiv;
+
+    // Emit links to files which don't contain any functions. These are normally
+    // not very useful, but could be relevant for code which abuses the
+    // preprocessor.
+    if (!EmptyFiles.empty()) {
+      OSRef << tag("p", "Files which contain no functions. (These "
+                        "files contain code pulled into other files "
+                        "by the preprocessor.)\n");
+      OSRef << BeginCenteredDiv << BeginTable;
+      for (auto FCS : EmptyFiles) {
+        auto Link = buildRelLinkToFile(FCS->Name);
+        OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
+      }
+      OSRef << EndTable << EndCenteredDiv;
+    }
+
+    // Emit epilog.
+    OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));
+    emitEpilog(OSRef);
+
+    return Error::success();
+  }
+
+  /// Make a title with hyperlinks to the index.html files of each hierarchy
+  /// of the report.
+  std::string buildTitleLinks(StringRef LCPath) const {
+    // For each report level in LCPStack, extract the path component and
+    // calculate the number of "../" relative to current LCPath.
+    SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components;
+
+    auto Iter = LCPStack.begin(), IterE = LCPStack.end();
+    SmallString<128> RootPath;
+    if (*Iter == 0) {
+      // If llvm-cov works on relative coverage mapping data, the LCP of
+      // all source file paths can be 0, which makes the title path empty.
+      // As we like adding a slash at the back of the path to indicate a
+      // directory, in this case, we use "." as the root path to make it
+      // not be confused with the root path "/".
+      RootPath = ".";
+    } else {
+      RootPath = LCPath.substr(0, *Iter);
+      sys::path::native(RootPath);
+      sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true);
+    }
+    Components.emplace_back(std::move(RootPath), 0);
+
+    for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
+      SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last);
+      sys::path::native(SubPath);
+      sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true);
+      auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1;
+      Components.back().second += Level;
+      Components.emplace_back(std::move(SubPath), Level);
+    }
+
+    // Then we make the title accroding to Components.
+    std::string S;
+    for (auto I = Components.begin(), E = Components.end();;) {
+      auto &Name = I->first;
+      if (++I == E) {
+        S += a("./index.html", Name);
+        S += sys::path::get_separator();
+        break;
+      }
+
+      SmallString<128> Link;
+      for (unsigned J = I->second; J > 0; --J)
+        Link += "../";
+      Link += "index.html";
+      S += a(Link, Name);
+      S += sys::path::get_separator();
+    }
+    return S;
+  }
+
+  std::string buildRelLinkToFile(StringRef RelPath) const {
+    SmallString<128> LinkTextStr(RelPath);
+    sys::path::native(LinkTextStr);
+
+    // remove_dots will remove trailing slash, so we need to check before it.
+    auto IsDir = LinkTextStr.endswith(sys::path::get_separator());
+    sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
+
+    SmallString<128> LinkTargetStr(LinkTextStr);
+    if (IsDir) {
+      LinkTextStr += sys::path::get_separator();
+      sys::path::append(LinkTargetStr, "index.html");
+    } else {
+      LinkTargetStr += ".html";
+    }
+
+    auto LinkText = escape(LinkTextStr, Options);
+    auto LinkTarget = escape(LinkTargetStr, Options);
+    return a(LinkTarget, LinkText);
+  }
+};
+
+Error CoveragePrinterHTMLDirectory::createIndexFile(
+    ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
+    const CoverageFiltersMatchAll &Filters) {
+  // The createSubIndexFile function only works when SourceFiles is
+  // more than one. So we fallback to CoveragePrinterHTML when it is.
+  if (SourceFiles.size() <= 1)
+    return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters);
+
+  // Emit the default stylesheet.
+  if (Error E = emitStyleSheet())
+    return E;
+
+  // Emit index files in every subdirectory.
+  Reporter Report(*this, Coverage, Filters);
+  auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
+  if (auto E = TotalsOrErr.takeError())
+    return E;
+  auto &LCPath = TotalsOrErr->Name;
+
+  // Emit the top level index file. Top level index file is just a redirection
+  // to the index file in the LCP directory.
+  auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
+  if (auto E = OSOrErr.takeError())
+    return E;
+  auto OS = std::move(OSOrErr.get());
+  auto LCPIndexFilePath =
+      getOutputPath((LCPath + "index").str(), "html", /*InToplevel=*/false);
+  *OS.get() << R"(<!DOCTYPE html>
+  <html>
+    <head>
+      <meta http-equiv="Refresh" content="0; url=')"
+            << LCPIndexFilePath << R"('" />
+    </head>
+    <body></body>
+  </html>
+  )";
+
+  return Error::success();
+}
+
 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
   OS << BeginCenteredDiv << BeginTable;
 }

diff  --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
index 7d94675f4b0b7c..c846379889cd3a 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
+++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h
@@ -19,6 +19,8 @@ namespace llvm {
 
 using namespace coverage;
 
+class ThreadPool;
+
 struct FileCoverageSummary;
 
 /// A coverage printer for html output.
@@ -36,6 +38,10 @@ class CoveragePrinterHTML : public CoveragePrinter {
   CoveragePrinterHTML(const CoverageViewOptions &Opts)
       : CoveragePrinter(Opts) {}
 
+protected:
+  Error emitStyleSheet();
+  void emitReportHeader(raw_ostream &OSRef, const std::string &Title);
+
 private:
   void emitFileSummary(raw_ostream &OS, StringRef SF,
                        const FileCoverageSummary &FCS,
@@ -44,6 +50,20 @@ class CoveragePrinterHTML : public CoveragePrinter {
                               const FileCoverageSummary &FCS) const;
 };
 
+/// A coverage printer for html output, but generates index files in every
+/// subdirectory to show a hierarchical view.
+class CoveragePrinterHTMLDirectory : public CoveragePrinterHTML {
+public:
+  using CoveragePrinterHTML::CoveragePrinterHTML;
+
+  Error createIndexFile(ArrayRef<std::string> SourceFiles,
+                        const coverage::CoverageMapping &Coverage,
+                        const CoverageFiltersMatchAll &Filters) override;
+
+private:
+  struct Reporter;
+};
+
 /// A code coverage view which supports html-based rendering.
 class SourceCoverageViewHTML : public SourceCoverageView {
   void renderViewHeader(raw_ostream &OS) override;

diff  --git a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
index 6e0db096a11c9f..44694a0426c81a 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
+++ b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp
@@ -14,7 +14,9 @@
 #include "CoverageReport.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Format.h"
+#include "llvm/Support/Path.h"
 #include <optional>
 
 using namespace llvm;
@@ -46,6 +48,69 @@ Error CoveragePrinterText::createIndexFile(
   return Error::success();
 }
 
+struct CoveragePrinterTextDirectory::Reporter : public DirectoryCoverageReport {
+  CoveragePrinterTextDirectory &Printer;
+
+  Reporter(CoveragePrinterTextDirectory &Printer,
+           const coverage::CoverageMapping &Coverage,
+           const CoverageFiltersMatchAll &Filters)
+      : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
+        Printer(Printer) {}
+
+  Error generateSubDirectoryReport(SubFileReports &&SubFiles,
+                                   SubDirReports &&SubDirs,
+                                   FileCoverageSummary &&SubTotals) override {
+    auto &LCPath = SubTotals.Name;
+    assert(Options.hasOutputDirectory() &&
+           "No output directory for index file");
+
+    SmallString<128> OSPath = LCPath;
+    sys::path::append(OSPath, "index");
+    auto OSOrErr = Printer.createOutputStream(OSPath, "txt",
+                                              /*InToplevel=*/false);
+    if (auto E = OSOrErr.takeError())
+      return E;
+    auto OS = std::move(OSOrErr.get());
+    raw_ostream &OSRef = *OS.get();
+
+    std::vector<FileCoverageSummary> Reports;
+    for (auto &&SubDir : SubDirs)
+      Reports.push_back(std::move(SubDir.second.first));
+    for (auto &&SubFile : SubFiles)
+      Reports.push_back(std::move(SubFile.second));
+
+    CoverageReport Report(Options, Coverage);
+    Report.renderFileReports(OSRef, Reports, SubTotals, Filters.empty());
+
+    Options.colored_ostream(OSRef, raw_ostream::CYAN)
+        << "\n"
+        << Options.getLLVMVersionString();
+
+    return Error::success();
+  }
+};
+
+Error CoveragePrinterTextDirectory::createIndexFile(
+    ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
+    const CoverageFiltersMatchAll &Filters) {
+  if (SourceFiles.size() <= 1)
+    return CoveragePrinterText::createIndexFile(SourceFiles, Coverage, Filters);
+
+  Reporter Report(*this, Coverage, Filters);
+  auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
+  if (auto E = TotalsOrErr.takeError())
+    return E;
+  auto &LCPath = TotalsOrErr->Name;
+
+  auto TopIndexFilePath =
+      getOutputPath("index", "txt", /*InToplevel=*/true, /*Relative=*/false);
+  auto LCPIndexFilePath =
+      getOutputPath((LCPath + "index").str(), "txt", /*InToplevel=*/false,
+                    /*Relative=*/false);
+  return errorCodeToError(
+      sys::fs::copy_file(LCPIndexFilePath, TopIndexFilePath));
+}
+
 namespace {
 
 static const unsigned LineCoverageColumnWidth = 7;

diff  --git a/llvm/tools/llvm-cov/SourceCoverageViewText.h b/llvm/tools/llvm-cov/SourceCoverageViewText.h
index b2be06039f9572..ade47ed3b5f586 100644
--- a/llvm/tools/llvm-cov/SourceCoverageViewText.h
+++ b/llvm/tools/llvm-cov/SourceCoverageViewText.h
@@ -35,6 +35,21 @@ class CoveragePrinterText : public CoveragePrinter {
       : CoveragePrinter(Opts) {}
 };
 
+/// A coverage printer for text output, but generates index files in every
+/// subdirectory to show a hierarchical view. The implementation is similar
+/// to CoveragePrinterHTMLDirectory. So please refer to that for more comments.
+class CoveragePrinterTextDirectory : public CoveragePrinterText {
+public:
+  using CoveragePrinterText::CoveragePrinterText;
+
+  Error createIndexFile(ArrayRef<std::string> SourceFiles,
+                        const CoverageMapping &Coverage,
+                        const CoverageFiltersMatchAll &Filters) override;
+
+private:
+  struct Reporter;
+};
+
 /// A code coverage view which supports text-based rendering.
 class SourceCoverageViewText : public SourceCoverageView {
   void renderViewHeader(raw_ostream &OS) override;

diff  --git a/llvm/tools/llvm-cov/TestingSupport.cpp b/llvm/tools/llvm-cov/TestingSupport.cpp
index 59e9ee402b9162..6ad8c35d6e12f8 100644
--- a/llvm/tools/llvm-cov/TestingSupport.cpp
+++ b/llvm/tools/llvm-cov/TestingSupport.cpp
@@ -121,6 +121,11 @@ int convertForTestingMain(int argc, const char *argv[]) {
     return 1;
   }
 
+  // If this is a linked PE/COFF file, then we have to skip over the null byte
+  // that is allocated in the .lprfn$A section in the LLVM profiling runtime.
+  if (isa<COFFObjectFile>(OF) && !OF->isRelocatableObject())
+    ProfileNamesData = ProfileNamesData.drop_front(1);
+
   int FD;
   if (auto Err = sys::fs::openFileForWrite(OutputFilename, FD)) {
     errs() << "error: " << Err.message() << "\n";


        


More information about the llvm-commits mailing list