toolchain: upload Compiler Rusage Logs

We need to upload the resource usage for compiler invocations
in order to monitor for regressions.

BUG=chromium:1156314
TEST=./run_pytest

Change-Id: I16c8ecc39899cd1dbf6ffe936bf68f64745ddca7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2582963
Tested-by: Ryan Beltran <ryanbeltran@chromium.org>
Commit-Queue: Ryan Beltran <ryanbeltran@chromium.org>
Reviewed-by: Tiancong Wang <tcwang@google.com>
Reviewed-by: Alex Klein <saklein@chromium.org>
Reviewed-by: George Burgess <gbiv@chromium.org>
Reviewed-by: LaMont Jones <lamontjones@chromium.org>
diff --git a/api/controller/toolchain.py b/api/controller/toolchain.py
index bbab194..4007f10 100644
--- a/api/controller/toolchain.py
+++ b/api/controller/toolchain.py
@@ -76,6 +76,9 @@
     BuilderConfig.Artifacts.CLANG_CRASH_DIAGNOSES:
         _Handlers('ClangCrashDiagnoses', toolchain_util.PrepareForBuild,
                   toolchain_util.BundleArtifacts),
+    BuilderConfig.Artifacts.COMPILER_RUSAGE_LOG:
+        _Handlers('CompilerRusageLogs', toolchain_util.PrepareForBuild,
+                  toolchain_util.BundleArtifacts),
 }
 
 
diff --git a/lib/toolchain_util.py b/lib/toolchain_util.py
index ca24cf6..5c10686 100644
--- a/lib/toolchain_util.py
+++ b/lib/toolchain_util.py
@@ -203,6 +203,10 @@
   """Error for BundleArtifactsHandler class."""
 
 
+class NoArtifactsToBundleError(Error):
+  """Error for bundling empty collection of artifacts."""
+
+
 class GenerateChromeOrderfileError(Error):
   """Error for GenerateChromeOrderfile class."""
 
@@ -2094,6 +2098,12 @@
     self._CleanupArtifactDirectory('/tmp/clang_crash_diagnostics')
     return PrepareForBuildReturn.UNKNOWN
 
+  def _PrepareCompilerRusageLogs(self):
+    # We always build this artifact.
+    # Cleanup the temp directory that holds the artifacts
+    self._CleanupArtifactDirectory('/tmp/compiler_rusage')
+    return PrepareForBuildReturn.UNKNOWN
+
 
 class BundleArtifactHandler(_CommonPrepareBundle):
   """Methods for updating ebuilds for toolchain artifacts."""
@@ -2456,42 +2466,87 @@
     logging.info('%d files collected', len(output))
     return output
 
+  def _CreateBundle(self, src_dir, tarball, destination, extension=None):
+    """Bundle the files from src_dir into a tar.xz file.
+
+    Args:
+      src_dir: the path to the directory to copy files from.
+      tarball: name of the generated tarballfile (build target, time stamp,
+        and .tar.xz extension will be added automatically)
+      destination: path to create tarball in
+      extension: type of file to search for in src_dir.
+        If extension is None (default), all file types will be allowed.
+
+    Returns:
+      Path to the generated tar.xz file
+    """
+
+    def FilterFile(file_path):
+      return extension is None or file_path.endswith(extension)
+
+    files = self._CollectFiles(
+        src_dir,
+        destination,
+        include_file=FilterFile)
+    if not files:
+      logging.info('No data found for %s, skip bundle artifact', tarball)
+      raise NoArtifactsToBundleError(f'No {extension} files in {src_dir}')
+
+    now = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d')
+    name = f'{self.build_target}.{now}.{tarball}.tar.xz'
+    output_compressed = os.path.join(self.output_dir, name)
+    cros_build_lib.CreateTarball(output_compressed, destination, inputs=files)
+
+    return output_compressed
+
   def _BundleToolchainWarningLogs(self):
     """Bundle the compiler warnings for upload for werror checker."""
     with self.chroot.tempdir() as tempdir:
-      warning_files = self._CollectFiles(
-          '/tmp/fatal_clang_warnings',
-          tempdir,
-          include_file=lambda file_path: file_path.endswith('.json'))
-
-      if not warning_files:
-        logging.info('No fatal-clang-warnings found, skip bundle artifact')
+      try:
+        return [
+          self._CreateBundle(
+            '/tmp/fatal_clang_warnings',
+            'fatal_clang_warnings',
+            tempdir,
+            '.json')
+        ]
+      except NoArtifactsToBundleError:
         return []
-      now = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d')
-      name = f'{self.build_target}.{now}.fatal_clang_warnings.tar.xz'
-      output_compressed = os.path.join(self.output_dir, name)
-      cros_build_lib.CreateTarball(
-          output_compressed, tempdir, inputs=warning_files)
-
-    return [output_compressed]
 
   def _BundleClangCrashDiagnoses(self):
-    """Bundle all clang crash diagnoses in chroot for uploading."""
-    with osutils.TempDir(prefix='clang_crash_diagnoses_tarball') as tempdir:
-      diagnoses = self._CollectFiles(
-          '/tmp/clang_crash_diagnostics', tempdir, include_file=lambda _: True)
+    """Bundle all clang crash diagnoses in chroot for uploading.
 
-      if not diagnoses:
-        logging.info('No clang crashes found, skip bundle artifact')
+      See bugs.chromium.org/p/chromium/issues/detail?id=1056904 for context.
+    """
+    with osutils.TempDir(prefix='clang_crash_diagnoses_tarball') as tempdir:
+      try:
+        return [
+          self._CreateBundle(
+            '/tmp/clang_crash_diagnostics',
+            'clang_crash_diagnoses',
+            tempdir)
+        ]
+      except NoArtifactsToBundleError:
         return []
 
-      now = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d')
-      output = os.path.join(
-          self.output_dir,
-          f'{self.build_target}.{now}.clang_crash_diagnoses.tar.xz')
-      cros_build_lib.CreateTarball(output, tempdir, inputs=diagnoses)
-      return [output]
+  def _BundleCompilerRusageLogs(self):
+    """Bundle the rusage files created by compiler invocations.
 
+    This is useful for monitoring changes in compiler performance.
+    These files are created when the TOOLCHAIN_RUSAGE_OUTPUT variable
+    is set in the environment for monitoring compiler performance.
+    """
+    with self.chroot.tempdir() as tempdir:
+      try:
+        return [
+          self._CreateBundle(
+            '/tmp/compiler_rusage',
+            'compiler_rusage_logs',
+            tempdir,
+            '.json')
+        ]
+      except NoArtifactsToBundleError:
+        return []
 
 def PrepareForBuild(artifact_name, chroot, sysroot_path, build_target,
                     input_artifacts, profile_info):
diff --git a/lib/toolchain_util_unittest.py b/lib/toolchain_util_unittest.py
index 8bdde76..643016d 100644
--- a/lib/toolchain_util_unittest.py
+++ b/lib/toolchain_util_unittest.py
@@ -751,6 +751,8 @@
                              expected_output_files):
     """Asserts that the given artifact_path is tarred up properly.
 
+    If no output files are expected, we assert that no tarballs are created.
+
     Args:
       artifact_path: the path to touch |input_files| in.
       tarball_name: the expected name of the tarball we will produce.
@@ -774,20 +776,32 @@
           osutils.Touch(p)
 
       tarball = self.obj.Bundle()
-      tarball_path = os.path.join(self.outdir, tarball_name)
-      self.assertEqual(tarball, [tarball_path])
 
-      create_tarball_mock.assert_called_once()
-      output, _tempdir = create_tarball_mock.call_args[0]
-      self.assertEqual(output, tarball_path)
-      inputs = create_tarball_mock.call_args[1]['inputs']
-      self.assertCountEqual(expected_output_files, inputs)
+      if len(expected_output_files) > 0:
+        tarball_path = os.path.join(self.outdir, tarball_name)
+        self.assertEqual(tarball, [tarball_path])
+
+        create_tarball_mock.assert_called_once()
+        output, _tempdir = create_tarball_mock.call_args[0]
+        self.assertEqual(output, tarball_path)
+        inputs = create_tarball_mock.call_args[1]['inputs']
+        self.assertCountEqual(expected_output_files, inputs)
+      else:
+        # Bundlers do not create tarballs when no artifacts are found.
+        self.assertEqual(tarball, [])
 
   def testBundleToolchainWarningLogs(self):
     self.SetUpBundle('ToolchainWarningLogs')
+    artifact_path = '/tmp/fatal_clang_warnings'
+    tarball_name = '%s.DATE.fatal_clang_warnings.tar.xz' % self.board
+
+    # Test behaviour when no artifacts are found.
+    self.runToolchainBundleTest(artifact_path, tarball_name, [], [])
+
+    # Test behaviour when artifacts are found.
     self.runToolchainBundleTest(
-        artifact_path='/tmp/fatal_clang_warnings',
-        tarball_name='%s.DATE.fatal_clang_warnings.tar.xz' % self.board,
+        artifact_path,
+        tarball_name,
         input_files=('log1.json', 'log2.json', 'log3.notjson', 'log4'),
         expected_output_files=(
             'log1.json',
@@ -799,9 +813,16 @@
 
   def testBundleClangCrashDiagnoses(self):
     self.SetUpBundle('ClangCrashDiagnoses')
+    artifact_path = '/tmp/clang_crash_diagnostics'
+    tarball_name = '%s.DATE.clang_crash_diagnoses.tar.xz' % self.board
+
+    # Test behaviour when no artifacts are found.
+    self.runToolchainBundleTest(artifact_path, tarball_name, [], [])
+
+    # Test behaviour when artifacts are found.
     self.runToolchainBundleTest(
-        artifact_path='/tmp/clang_crash_diagnostics',
-        tarball_name='%s.DATE.clang_crash_diagnoses.tar.xz' % self.board,
+        artifact_path,
+        tarball_name,
         input_files=('1.cpp', '1.sh', '2.cc', '2.sh', 'foo/bar.sh'),
         expected_output_files=(
             '1.cpp',
@@ -817,6 +838,32 @@
         ),
     )
 
+  def testBundleCompilerRusageLogs(self):
+    self.SetUpBundle('CompilerRusageLogs')
+    artifact_path = '/tmp/compiler_rusage'
+    tarball_name = '%s.DATE.compiler_rusage_logs.tar.xz' % self.board
+
+    # Test behaviour when no artifacts are found.
+    self.runToolchainBundleTest(artifact_path, tarball_name, [], [])
+
+    # Test behaviour when artifacts are found.
+    self.runToolchainBundleTest(
+      artifact_path,
+      tarball_name,
+      input_files=(
+        'good1.json', 'good2.json', 'good3.json',
+        'bad1.notjson', 'bad2', 'json',
+      ),
+      expected_output_files=(
+          'good1.json',
+          'good2.json',
+          'good3.json',
+          'good10.json',
+          'good20.json',
+          'good30.json',
+      ),
+    )
+
 
 class UpdateKernelMetadataTest(PrepareBundleTest):
   """Test _UpdateKernelMetadata() function in BundleArtifactHandler."""