1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4import errno, os, re, subprocess
5from autotest_lib.client.bin import test
6from autotest_lib.client.common_lib import error
7
8
9class platform_Perf(test.test):
10    """
11    Gathers perf data and makes sure it is well-formed.
12    """
13    version = 1
14
15    # Whitelist of DSOs that are expected to appear in perf data from a CrOS
16    # device. The actual name may change so use regex pattern matching. This is
17    # a list of allowed but not required DSOs. With this list, we can filter out
18    # unknown DSOs that might not have a build ID, e.g. JIT code.
19    _KERNEL_NAME_REGEX = re.compile(r'.*kernel\.kallsyms.*')
20    _DSO_WHITELIST_REGEX = [
21      _KERNEL_NAME_REGEX,
22      re.compile(r'bash'),
23      re.compile(r'chrome'),
24      re.compile(r'ld-.*\.so.*'),
25      re.compile(r'libbase.*\.so.*'),
26      re.compile(r'libc.*\.so.*'),
27      re.compile(r'libdbus.*\.so.*'),
28      re.compile(r'libpthread.*\.so.*'),
29      re.compile(r'libstdc\+\+.*\.so.*'),
30    ]
31
32
33    def run_once(self):
34        """
35        Collect a perf data profile and check the detailed perf report.
36        """
37        keyvals = {}
38        num_errors = 0
39
40        try:
41            # Create temporary file and get its name. Then close it.
42            perf_file_path = os.tempnam()
43
44            # Perf command for recording a profile.
45            perf_record_args = [ 'perf', 'record', '-a', '-o', perf_file_path,
46                                 '--', 'sleep', '2']
47            # Perf command for getting a detailed report.
48            perf_report_args = [ 'perf', 'report', '-D', '-i', perf_file_path ]
49            # Perf command for getting a report grouped by DSO name.
50            perf_report_dso_args = [ 'perf', 'report', '--sort', 'dso', '-i',
51                                     perf_file_path ]
52            # Perf command for getting a list of all build IDs in a data file.
53            perf_buildid_list_args = [ 'perf', 'buildid-list', '-i',
54                                       perf_file_path ]
55
56            try:
57                subprocess.check_output(perf_record_args,
58                                        stderr=subprocess.STDOUT)
59            except subprocess.CalledProcessError as cmd_error:
60                raise error.TestFail("Running command [%s] failed: %s" %
61                                     (' '.join(perf_record_args),
62                                      cmd_error.output))
63
64            # Make sure the file still exists.
65            if not os.path.isfile(perf_file_path):
66                raise error.TestFail('Could not find perf output file: ' +
67                                     perf_file_path)
68
69            # Get detailed perf data view and extract the line containing the
70            # kernel MMAP summary.
71            kernel_mapping = None
72            p = subprocess.Popen(perf_report_args, stdout=subprocess.PIPE)
73            for line in p.stdout:
74                if ('PERF_RECORD_MMAP' in line and
75                    self._KERNEL_NAME_REGEX.match(line)):
76                    kernel_mapping = line
77                    break
78
79            # Read the rest of output to EOF.
80            for _ in p.stdout:
81                pass
82            p.wait();
83
84            # Generate a list of whitelisted DSOs from the perf report.
85            dso_list = []
86            p = subprocess.Popen(perf_report_dso_args, stdout=subprocess.PIPE)
87            for line in p.stdout:
88                # Skip comments.
89                if line.startswith('#'):
90                    continue
91                # The output consists of percentage and DSO name.
92                tokens = line.split()
93                if len(tokens) < 2:
94                    continue
95                # Store the DSO name if it appears in the whitelist.
96                dso_name = tokens[1]
97                for regex in self._DSO_WHITELIST_REGEX:
98                    if regex.match(dso_name):
99                        dso_list += [dso_name]
100
101            p.wait();
102
103            # Generate a mapping of DSOs to their build IDs.
104            dso_to_build_ids = {}
105            p = subprocess.Popen(perf_buildid_list_args, stdout=subprocess.PIPE)
106            for line in p.stdout:
107                # The output consists of build ID and DSO name.
108                tokens = line.split()
109                if len(tokens) < 2:
110                    continue
111                # The build ID list uses the full path of the DSOs, while the
112                # report output usesonly the basename. Store the basename to
113                # make lookups easier.
114                dso_to_build_ids[os.path.basename(tokens[1])] = tokens[0]
115
116            p.wait();
117
118
119        finally:
120            # Delete the perf data file.
121            try:
122                os.remove(perf_file_path)
123            except OSError as e:
124                if e.errno != errno.ENONENT: raise
125
126        if kernel_mapping is None:
127            raise error.TestFail('Could not find kernel mapping in perf '
128                                 'report.')
129        # Get the kernel mapping values.
130        kernel_mapping = kernel_mapping.split(':')[2]
131        start, length, pgoff = re.sub(r'[][()@]', ' ',
132                                      kernel_mapping).strip().split()
133
134        # Check that all whitelisted DSOs from the report have build IDs.
135        kernel_name = None
136        kernel_build_id = None
137        for dso in dso_list:
138            if dso not in dso_to_build_ids:
139                raise error.TestFail('Could not find build ID for %s' % dso)
140            if self._KERNEL_NAME_REGEX.match(dso):
141                kernel_name = dso
142                kernel_build_id = dso_to_build_ids[dso]
143
144        # Make sure the kernel build ID was found.
145        if not kernel_build_id:
146            raise error.TestFail('Could not find kernel entry (containing '
147                                 '"%s") in build ID list' % self._KERNEL_NAME)
148
149        # Write keyvals.
150        keyvals = {}
151        keyvals['start'] = start
152        keyvals['length'] = length
153        keyvals['pgoff'] = pgoff
154        keyvals['kernel_name'] = kernel_name
155        keyvals['kernel_build_id'] = kernel_build_id
156        self.write_perf_keyval(keyvals)
157
158        # Make sure that the kernel mapping values follow an expected pattern,
159        #
160        # Expect one of two patterns:
161        # (1) start == pgoff, e.g.:
162        #   start=0x80008200
163        #   pgoff=0x80008200
164        #   len  =0xfffffff7ff7dff
165        # (2) start < pgoff < start + len, e.g.:
166        #   start=0x3bc00000
167        #   pgoff=0xffffffffbcc00198
168        #   len  =0xffffffff843fffff
169        start = int(start, 0)
170        length = int(length, 0)
171        pgoff = int(pgoff, 0)
172        if not (start == pgoff or start < pgoff < start + length):
173            raise error.TestFail('Improper kernel mapping values!')
174