[Code Coverage] Add support for component build + update documentation.

Bug: 831939
Change-Id: I2804796894045dc69aa8309aa3ab34e6c80b38d7
Reviewed-on: https://chromium-review.googlesource.com/1010464
Commit-Queue: Max Moroz <[email protected]>
Reviewed-by: Yuke Liao <[email protected]>
Reviewed-by: Abhishek Arya <[email protected]>
Cr-Commit-Position: refs/heads/master@{#553211}
diff --git a/build/config/coverage/coverage.gni b/build/config/coverage/coverage.gni
index ee4734cf..61716dd 100644
--- a/build/config/coverage/coverage.gni
+++ b/build/config/coverage/coverage.gni
@@ -5,21 +5,9 @@
 import("//build/toolchain/toolchain.gni")
 
 declare_args() {
-  # Enable Clang's Source-based Code Coverage. This should not be used with
-  # is_component_build=true.
-  # TODO(metzman): Add an assertion that will prevent is_component_build=true
-  # from being passed as an argument to gn, but will not force toolchains that
-  # set is_component_build=true to set it to false.
+  # Enable Clang's Source-based Code Coverage.
   use_clang_coverage = false
 }
 
 assert(!use_clang_coverage || is_clang,
        "Clang Source-based Code Coverage requires clang.")
-
-assert(
-    !(use_clang_coverage && is_component_build),
-    "Clang Source-based Code Coverage requires \"is_component_build=false\" " +
-        "flag because: There will be no coverage info for libraries in " +
-        "component builds and \"is_component_build\" is set to true by " +
-        "\"is_debug\" unless it is explicitly set to false.$0x0ATry adding " +
-        "\"is_component_build=false\" to your GN args.")
diff --git a/docs/code_coverage.md b/docs/code_coverage.md
index bbc7e8b8..0245e80 100644
--- a/docs/code_coverage.md
+++ b/docs/code_coverage.md
@@ -8,6 +8,8 @@
   * [Step 2 Create Raw Profiles](#step-2-create-raw-profiles)
   * [Step 3 Create Indexed Profile](#step-3-create-indexed-profile)
   * [Step 4 Create Coverage Reports](#step-4-create-coverage-reports)
+- [Contacts](#contacts)
+- [FAQ](#faq)
 
 Chromium uses Clang source-based code coverage, this [documentation] explains
 how to use Clang’s source-based coverage features in general.
@@ -135,12 +137,24 @@
 
 For more information on how to use llvm-cov, please refer to the [guide].
 
-## Reporting problems
+## Contacts
+
+### Reporting problems
 For any breakage report and feature requests, please [file a bug].
 
-## Mail list
+### Mailing list
 For questions and general discussions, please join [chrome-code-coverage group].
 
+## FAQ
+
+### Can I use `is_component_build=true` for code coverage build?
+
+Yes, code coverage instrumentation works with both component and non-component
+builds. Component build is usually faster to compile, but can be up to several
+times slower to run with code coverage instrumentation. For more information,
+see [crbug.com/831939](https://crbug.com/831939).
+
+
 [documentation]: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
 [coverage script]: https://cs.chromium.org/chromium/src/tools/code_coverage/coverage.py
 [code coverage report directory view]: images/code_coverage_directory_view.png
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index a6a1c65..6d8b6e4 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -313,6 +313,50 @@
       html_file.write(html_header + html_table + html_footer)
 
 
+def _GetSharedLibraries(binary_paths):
+  """Returns set of shared libraries used by specified binaries."""
+  libraries = set()
+  cmd = []
+  shared_library_re = None
+
+  if sys.platform.startswith('linux'):
+    cmd.extend(['ldd'])
+    shared_library_re = re.compile(
+        r'.*\.so\s=>\s(.*' + BUILD_DIR + '.*\.so)\s.*')
+  elif sys.platform.startswith('darwin'):
+    cmd.extend(['otool', '-L'])
+    shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
+  else:
+    assert False, ('Cannot detect shared libraries used by the given targets.')
+
+  assert shared_library_re is not None
+
+  cmd.extend(binary_paths)
+  output = subprocess.check_output(cmd)
+
+  for line in output.splitlines():
+    m = shared_library_re.match(line)
+    if not m:
+      continue
+
+    shared_library_path = m.group(1)
+    if sys.platform.startswith('darwin'):
+      # otool outputs "@rpath" macro instead of the dirname of the given binary.
+      shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
+
+    assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
+                                                 'the given target(s) does not '
+                                                 'exist.' % shared_library_path)
+    with open(shared_library_path) as f:
+      data = f.read()
+
+    # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
+    if '__llvm_cov' in data:
+      libraries.add(shared_library_path)
+
+  return list(libraries)
+
+
 def _GetHostPlatform():
   """Returns the host platform.
 
@@ -823,23 +867,25 @@
   """Runs a single command and generates a profraw data file."""
   # Per Clang "Source-based Code Coverage" doc:
   #
-  # "%p" expands out to the process ID.
+  # "%p" expands out to the process ID. It's not used by this scripts due to:
+  # 1) If a target program spawns too many processess, it may exhaust all disk
+  #    space available. For example, unit_tests writes thousands of .profraw
+  #    files each of size 1GB+.
+  # 2) If a target binary uses shared libraries, coverage profile data for them
+  #    will be missing, resulting in incomplete coverage reports.
   #
   # "%Nm" expands out to the instrumented binary's signature. When this pattern
   # is specified, the runtime creates a pool of N raw profiles which are used
   # for on-line profile merging. The runtime takes care of selecting a raw
   # profile from the pool, locking it, and updating it before the program exits.
-  # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
   # N must be between 1 and 9. The merge pool specifier can only occur once per
   # filename pattern.
   #
-  # "%p" is used when tests run in single process, however, it can't be used for
-  # multi-process because each process produces an intermediate dump, which may
-  # consume hundreds of gigabytes of disk space.
+  # "%1m" is used when tests run in single process, such as fuzz targets.
   #
-  # For "%Nm", 4 is chosen because it creates some level of parallelism, but
-  # it's not too big to consume too much computing resource or disk space.
-  profile_pattern_string = '%p' if _IsFuzzerTarget(target) else '%4m'
+  # For other cases, "%4m" is chosen as it creates some level of parallelism,
+  # but it's not too big to consume too much computing resource or disk space.
+  profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
   expected_profraw_file_name = os.extsep.join(
       [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
   expected_profraw_file_path = os.path.join(OUTPUT_DIR,
@@ -1262,6 +1308,7 @@
 
   logging.info('Generating code coverage report in html (this can take a while '
                'depending on size of target!)')
+  binary_paths.extend(_GetSharedLibraries(binary_paths))
   per_file_coverage_summary = _GeneratePerFileCoverageSummary(
       binary_paths, profdata_file_path, absolute_filter_paths,
       args.ignore_filename_regex)