platform_Perf: Check for build IDs in perf data

Expand test coverage to check the DSOs and their associated build IDs.
The perf data must contain build IDs for all the DSOs with samples (i.e.
the ones that show up in the report). A kernel build ID must also be
present in perf data.

BUG=chromium:584475
TEST=autotest platform_Perf passes, but fails with a perf binary built
with https://chromium-review.googlesource.com/#/c/326431/ reverted.
CQ-DEPEND=I08b87c9ac43672f65b1749600ddf2977e950c6bd

Change-Id: I4e7c81c7a42fb79958317e5f6a810a4b9e59b263
Signed-off-by: Simon Que <sque@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/326500
diff --git a/client/site_tests/platform_Perf/platform_Perf.py b/client/site_tests/platform_Perf/platform_Perf.py
index c557ca4..b593190 100644
--- a/client/site_tests/platform_Perf/platform_Perf.py
+++ b/client/site_tests/platform_Perf/platform_Perf.py
@@ -1,7 +1,7 @@
 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
-import os, re, subprocess
+import errno, os, re, subprocess
 from autotest_lib.client.bin import test
 from autotest_lib.client.common_lib import error
 
@@ -12,6 +12,23 @@
     """
     version = 1
 
+    # Whitelist of DSOs that are expected to appear in perf data from a CrOS
+    # device. The actual name may change so use regex pattern matching. This is
+    # a list of allowed but not required DSOs. With this list, we can filter out
+    # unknown DSOs that might not have a build ID, e.g. JIT code.
+    _KERNEL_NAME_REGEX = re.compile(r'.*kernel\.kallsyms.*')
+    _DSO_WHITELIST_REGEX = [
+      _KERNEL_NAME_REGEX,
+      re.compile(r'bash'),
+      re.compile(r'chrome'),
+      re.compile(r'ld-.*\.so.*'),
+      re.compile(r'libbase.*\.so.*'),
+      re.compile(r'libc.*\.so.*'),
+      re.compile(r'libdbus.*\.so.*'),
+      re.compile(r'libpthread.*\.so.*'),
+      re.compile(r'libstdc\+\+.*\.so.*'),
+    ]
+
 
     def run_once(self):
         """
@@ -29,6 +46,12 @@
                                  '--', 'sleep', '2']
             # Perf command for getting a detailed report.
             perf_report_args = [ 'perf', 'report', '-D', '-i', perf_file_path ]
+            # Perf command for getting a report grouped by DSO name.
+            perf_report_dso_args = [ 'perf', 'report', '--sort', 'dso', '-i',
+                                     perf_file_path ]
+            # Perf command for getting a list of all build IDs in a data file.
+            perf_buildid_list_args = [ 'perf', 'buildid-list', '-i',
+                                       perf_file_path ]
 
             try:
                 subprocess.check_output(perf_record_args,
@@ -45,18 +68,54 @@
 
             # Get detailed perf data view and extract the line containing the
             # kernel MMAP summary.
-            result = None
+            kernel_mapping = None
             p = subprocess.Popen(perf_report_args, stdout=subprocess.PIPE)
             for line in p.stdout:
-                if 'PERF_RECORD_MMAP' in line and 'kallsyms' in line:
-                    result = line
-                    break;
+                if ('PERF_RECORD_MMAP' in line and
+                    self._KERNEL_NAME_REGEX.match(line)):
+                    kernel_mapping = line
+                    break
 
             # Read the rest of output to EOF.
             for _ in p.stdout:
                 pass
             p.wait();
 
+            # Generate a list of whitelisted DSOs from the perf report.
+            dso_list = []
+            p = subprocess.Popen(perf_report_dso_args, stdout=subprocess.PIPE)
+            for line in p.stdout:
+                # Skip comments.
+                if line.startswith('#'):
+                    continue
+                # The output consists of percentage and DSO name.
+                tokens = line.split()
+                if len(tokens) < 2:
+                    continue
+                # Store the DSO name if it appears in the whitelist.
+                dso_name = tokens[1]
+                for regex in self._DSO_WHITELIST_REGEX:
+                    if regex.match(dso_name):
+                        dso_list += [dso_name]
+
+            p.wait();
+
+            # Generate a mapping of DSOs to their build IDs.
+            dso_to_build_ids = {}
+            p = subprocess.Popen(perf_buildid_list_args, stdout=subprocess.PIPE)
+            for line in p.stdout:
+                # The output consists of build ID and DSO name.
+                tokens = line.split()
+                if len(tokens) < 2:
+                    continue
+                # The build ID list uses the full path of the DSOs, while the
+                # report output usesonly the basename. Store the basename to
+                # make lookups easier.
+                dso_to_build_ids[os.path.basename(tokens[1])] = tokens[0]
+
+            p.wait();
+
+
         finally:
             # Delete the perf data file.
             try:
@@ -64,18 +123,36 @@
             except OSError as e:
                 if e.errno != errno.ENONENT: raise
 
-        if result is None:
+        if kernel_mapping is None:
             raise error.TestFail('Could not find kernel mapping in perf '
                                  'report.')
         # Get the kernel mapping values.
-        result = result.split(':')[2]
-        start, length, pgoff = re.sub(r'[][()@]', ' ', result).strip().split()
+        kernel_mapping = kernel_mapping.split(':')[2]
+        start, length, pgoff = re.sub(r'[][()@]', ' ',
+                                      kernel_mapping).strip().split()
+
+        # Check that all whitelisted DSOs from the report have build IDs.
+        kernel_name = None
+        kernel_build_id = None
+        for dso in dso_list:
+            if dso not in dso_to_build_ids:
+                raise error.TestFail('Could not find build ID for %s' % dso)
+            if self._KERNEL_NAME_REGEX.match(dso):
+                kernel_name = dso
+                kernel_build_id = dso_to_build_ids[dso]
+
+        # Make sure the kernel build ID was found.
+        if not kernel_build_id:
+            raise error.TestFail('Could not find kernel entry (containing '
+                                 '"%s") in build ID list' % self._KERNEL_NAME)
 
         # Write keyvals.
         keyvals = {}
         keyvals['start'] = start
         keyvals['length'] = length
         keyvals['pgoff'] = pgoff
+        keyvals['kernel_name'] = kernel_name
+        keyvals['kernel_build_id'] = kernel_build_id
         self.write_perf_keyval(keyvals)
 
         # Make sure that the kernel mapping values follow an expected pattern,