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      # For simplicity since the libbase binaries are built together, we assume
26      # that if one of them (libbase-core) was properly built and passes this
27      # test, then the others will pass as well. It's easier than trying to
28      # include all libbase-* while filtering out libbase-XXXXXX.so, which is a
29      # text file that links to the other files.
30      re.compile(r'libbase-core-.*\.so.*'),
31      re.compile(r'libc-.*\.so.*'),
32      re.compile(r'libdbus-.*\.so.*'),
33      re.compile(r'libpthread-.*\.so.*'),
34      re.compile(r'libstdc\+\+.*\.so\..*'),
35    ]
36
37
38    def run_once(self):
39        """
40        Collect a perf data profile and check the detailed perf report.
41        """
42        keyvals = {}
43        num_errors = 0
44
45        try:
46            # Create temporary file and get its name. Then close it.
47            perf_file_path = os.tempnam()
48
49            # Perf command for recording a profile.
50            perf_record_args = [ 'perf', 'record', '-a', '-o', perf_file_path,
51                                 '--', 'sleep', '2']
52            # Perf command for getting a detailed report.
53            perf_report_args = [ 'perf', 'report', '-D', '-i', perf_file_path ]
54            # Perf command for getting a report grouped by DSO name.
55            perf_report_dso_args = [ 'perf', 'report', '--sort', 'dso', '-i',
56                                     perf_file_path ]
57            # Perf command for getting a list of all build IDs in a data file.
58            perf_buildid_list_args = [ 'perf', 'buildid-list', '-i',
59                                       perf_file_path ]
60
61            try:
62                subprocess.check_output(perf_record_args,
63                                        stderr=subprocess.STDOUT)
64            except subprocess.CalledProcessError as cmd_error:
65                raise error.TestFail("Running command [%s] failed: %s" %
66                                     (' '.join(perf_record_args),
67                                      cmd_error.output))
68
69            # Make sure the file still exists.
70            if not os.path.isfile(perf_file_path):
71                raise error.TestFail('Could not find perf output file: ' +
72                                     perf_file_path)
73
74            # Get detailed perf data view and extract the line containing the
75            # kernel MMAP summary.
76            kernel_mapping = None
77            p = subprocess.Popen(perf_report_args, stdout=subprocess.PIPE)
78            for line in p.stdout:
79                if ('PERF_RECORD_MMAP' in line and
80                    self._KERNEL_NAME_REGEX.match(line)):
81                    kernel_mapping = line
82                    break
83
84            # Read the rest of output to EOF.
85            for _ in p.stdout:
86                pass
87            p.wait();
88
89            # Generate a list of whitelisted DSOs from the perf report.
90            dso_list = []
91            p = subprocess.Popen(perf_report_dso_args, stdout=subprocess.PIPE)
92            for line in p.stdout:
93                # Skip comments.
94                if line.startswith('#'):
95                    continue
96                # The output consists of percentage and DSO name.
97                tokens = line.split()
98                if len(tokens) < 2:
99                    continue
100                # Store the DSO name if it appears in the whitelist.
101                dso_name = tokens[1]
102                for regex in self._DSO_WHITELIST_REGEX:
103                    if regex.match(dso_name):
104                        dso_list += [dso_name]
105
106            p.wait();
107
108            # Generate a mapping of DSOs to their build IDs.
109            dso_to_build_ids = {}
110            p = subprocess.Popen(perf_buildid_list_args, stdout=subprocess.PIPE)
111            for line in p.stdout:
112                # The output consists of build ID and DSO name.
113                tokens = line.split()
114                if len(tokens) < 2:
115                    continue
116                # The build ID list uses the full path of the DSOs, while the
117                # report output usesonly the basename. Store the basename to
118                # make lookups easier.
119                dso_to_build_ids[os.path.basename(tokens[1])] = tokens[0]
120
121            p.wait();
122
123
124        finally:
125            # Delete the perf data file.
126            try:
127                os.remove(perf_file_path)
128            except OSError as e:
129                if e.errno != errno.ENONENT: raise
130
131        if kernel_mapping is None:
132            raise error.TestFail('Could not find kernel mapping in perf '
133                                 'report.')
134        # Get the kernel mapping values.
135        kernel_mapping = kernel_mapping.split(':')[2]
136        start, length, pgoff = re.sub(r'[][()@]', ' ',
137                                      kernel_mapping).strip().split()
138
139        # Check that all whitelisted DSOs from the report have build IDs.
140        kernel_name = None
141        kernel_build_id = None
142        for dso in dso_list:
143            if dso not in dso_to_build_ids:
144                raise error.TestFail('Could not find build ID for %s' % dso)
145            if self._KERNEL_NAME_REGEX.match(dso):
146                kernel_name = dso
147                kernel_build_id = dso_to_build_ids[dso]
148
149        # Make sure the kernel build ID was found.
150        if not kernel_build_id:
151            raise error.TestFail('Could not find kernel entry (containing '
152                                 '"%s") in build ID list' % self._KERNEL_NAME)
153
154        # Write keyvals.
155        keyvals = {}
156        keyvals['start'] = start
157        keyvals['length'] = length
158        keyvals['pgoff'] = pgoff
159        keyvals['kernel_name'] = kernel_name
160        keyvals['kernel_build_id'] = kernel_build_id
161        self.write_perf_keyval(keyvals)
162
163        # Make sure that the kernel mapping values follow an expected pattern,
164        #
165        # Expect one of two patterns:
166        # (1) start == pgoff, e.g.:
167        #   start=0x80008200
168        #   pgoff=0x80008200
169        #   len  =0xfffffff7ff7dff
170        # (2) start < pgoff < start + len, e.g.:
171        #   start=0x3bc00000
172        #   pgoff=0xffffffffbcc00198
173        #   len  =0xffffffff843fffff
174        start = int(start, 0)
175        length = int(length, 0)
176        pgoff = int(pgoff, 0)
177        if not (start == pgoff or start < pgoff < start + length):
178            raise error.TestFail('Improper kernel mapping values!')
179