Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright (C) 2016 The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | # |
| 17 | |
| 18 | """annotate.py: annotate source files based on perf.data. |
| 19 | """ |
| 20 | |
| 21 | |
| 22 | import argparse |
| 23 | import os |
| 24 | import os.path |
| 25 | import shutil |
| 26 | import subprocess |
| 27 | import sys |
| 28 | |
| 29 | from simpleperf_report_lib import * |
| 30 | from utils import * |
| 31 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 32 | class SourceLine(object): |
| 33 | def __init__(self, file, function, line): |
| 34 | self.file = file |
| 35 | self.function = function |
| 36 | self.line = line |
| 37 | |
| 38 | @property |
| 39 | def file_key(self): |
| 40 | return self.file |
| 41 | |
| 42 | @property |
| 43 | def function_key(self): |
| 44 | return (self.file, self.function) |
| 45 | |
| 46 | @property |
| 47 | def line_key(self): |
| 48 | return (self.file, self.line) |
| 49 | |
| 50 | |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 51 | # TODO: using addr2line can't convert from function_start_address to |
| 52 | # source_file:line very well for java code. Because in .debug_line section, |
| 53 | # there is some distance between function_start_address and the address |
| 54 | # of the first instruction which can be mapped to source line. |
| 55 | class Addr2Line(object): |
| 56 | """collect information of how to map [dso_name,vaddr] to [source_file:line]. |
| 57 | """ |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 58 | def __init__(self, addr2line_path, symfs_dir=None): |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 59 | self.dso_dict = dict() |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 60 | self.addr2line_path = addr2line_path |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 61 | self.symfs_dir = symfs_dir |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 62 | |
| 63 | |
| 64 | def add_addr(self, dso_name, addr): |
| 65 | dso = self.dso_dict.get(dso_name) |
| 66 | if dso is None: |
| 67 | self.dso_dict[dso_name] = dso = dict() |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 68 | if addr not in dso: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 69 | dso[addr] = None |
| 70 | |
| 71 | |
| 72 | def convert_addrs_to_lines(self): |
| 73 | # store a list of source files |
| 74 | self.file_list = [] |
| 75 | # map from file to id with file_list[id] == file |
| 76 | self.file_dict = {} |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 77 | self.file_list.append('') |
| 78 | self.file_dict[''] = 0 |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 79 | |
| 80 | for dso_name in self.dso_dict.keys(): |
| 81 | self._convert_addrs_to_lines(dso_name, self.dso_dict[dso_name]) |
| 82 | self._combine_source_files() |
| 83 | |
| 84 | |
| 85 | def _convert_addrs_to_lines(self, dso_name, dso): |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 86 | dso_path = self._find_dso_path(dso_name) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 87 | if dso_path is None: |
| 88 | log_warning("can't find dso '%s'" % dso_name) |
| 89 | dso.clear() |
| 90 | return |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 91 | addrs = sorted(dso.keys()) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 92 | addr_str = [] |
| 93 | for addr in addrs: |
| 94 | addr_str.append('0x%x' % addr) |
| 95 | addr_str = '\n'.join(addr_str) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 96 | subproc = subprocess.Popen([self.addr2line_path, '-e', dso_path, '-aifC'], |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 97 | stdin=subprocess.PIPE, stdout=subprocess.PIPE) |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 98 | (stdoutdata, _) = subproc.communicate(str_to_bytes(addr_str)) |
| 99 | stdoutdata = bytes_to_str(stdoutdata) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 100 | stdoutdata = stdoutdata.strip().split('\n') |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 101 | if len(stdoutdata) < len(addrs): |
| 102 | log_fatal("addr2line didn't output enough lines") |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 103 | addr_pos = 0 |
| 104 | out_pos = 0 |
| 105 | while addr_pos < len(addrs) and out_pos < len(stdoutdata): |
| 106 | addr_line = stdoutdata[out_pos] |
| 107 | out_pos += 1 |
| 108 | assert addr_line[:2] == "0x" |
| 109 | assert out_pos < len(stdoutdata) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 110 | source_lines = [] |
| 111 | while out_pos < len(stdoutdata) and stdoutdata[out_pos][:2] != "0x": |
| 112 | function = stdoutdata[out_pos] |
| 113 | out_pos += 1 |
| 114 | assert out_pos < len(stdoutdata) |
Yabin Cui | cbddd38 | 2017-05-03 14:46:55 -0700 | [diff] [blame] | 115 | # Handle lines like "C:\Users\...\file:32". |
| 116 | items = stdoutdata[out_pos].rsplit(':', 1) |
| 117 | if len(items) != 2: |
| 118 | continue |
| 119 | (file, line) = items |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 120 | line = line.split()[0] # Remove comments after line number |
| 121 | out_pos += 1 |
| 122 | if file.find('?') != -1: |
| 123 | file = 0 |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 124 | else: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 125 | file = self._get_file_id(file) |
| 126 | if line.find('?') != -1: |
| 127 | line = 0 |
| 128 | else: |
| 129 | line = int(line) |
| 130 | source_lines.append(SourceLine(file, function, line)) |
Yabin Cui | cbddd38 | 2017-05-03 14:46:55 -0700 | [diff] [blame] | 131 | dso[addrs[addr_pos]] = source_lines |
| 132 | addr_pos += 1 |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 133 | assert addr_pos == len(addrs) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 134 | |
| 135 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 136 | def _get_file_id(self, file): |
| 137 | id = self.file_dict.get(file) |
| 138 | if id is None: |
| 139 | id = len(self.file_list) |
| 140 | self.file_list.append(file) |
| 141 | self.file_dict[file] = id |
| 142 | return id |
| 143 | |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 144 | def _combine_source_files(self): |
| 145 | """It is possible that addr2line gives us different names for the same |
| 146 | file, like: |
| 147 | /usr/local/.../src/main/jni/sudo-game-jni.cpp |
| 148 | sudo-game-jni.cpp |
| 149 | We'd better combine these two files. We can do it by combining |
| 150 | source files with no conflicts in path. |
| 151 | """ |
| 152 | # Collect files having the same filename. |
| 153 | filename_dict = dict() |
| 154 | for file in self.file_list: |
| 155 | index = max(file.rfind('/'), file.rfind(os.sep)) |
| 156 | filename = file[index+1:] |
| 157 | entry = filename_dict.get(filename) |
| 158 | if entry is None: |
| 159 | filename_dict[filename] = entry = [] |
| 160 | entry.append(file) |
| 161 | |
| 162 | # Combine files having the same filename and having no conflicts in path. |
| 163 | for filename in filename_dict.keys(): |
| 164 | files = filename_dict[filename] |
| 165 | if len(files) == 1: |
| 166 | continue |
| 167 | for file in files: |
| 168 | to_file = file |
| 169 | # Test if we can merge files[i] with another file having longer |
| 170 | # path. |
| 171 | for f in files: |
| 172 | if len(f) > len(to_file) and f.find(file) != -1: |
| 173 | to_file = f |
| 174 | if to_file != file: |
| 175 | from_id = self.file_dict[file] |
| 176 | to_id = self.file_dict[to_file] |
| 177 | self.file_list[from_id] = self.file_list[to_id] |
| 178 | |
| 179 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 180 | def get_sources(self, dso_name, addr): |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 181 | dso = self.dso_dict.get(dso_name) |
| 182 | if dso is None: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 183 | return [] |
| 184 | item = dso.get(addr, []) |
| 185 | source_lines = [] |
| 186 | for source in item: |
| 187 | source_lines.append(SourceLine(self.file_list[source.file], |
| 188 | source.function, source.line)) |
| 189 | return source_lines |
| 190 | |
| 191 | |
| 192 | def _find_dso_path(self, dso): |
| 193 | if dso[0] != '/' or dso == '//anon': |
| 194 | return None |
| 195 | if self.symfs_dir: |
| 196 | dso_path = os.path.join(self.symfs_dir, dso[1:]) |
| 197 | if os.path.isfile(dso_path): |
| 198 | return dso_path |
| 199 | if os.path.isfile(dso): |
| 200 | return dso |
| 201 | return None |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 202 | |
| 203 | |
| 204 | class Period(object): |
| 205 | """event count information. It can be used to represent event count |
| 206 | of a line, a function, a source file, or a binary. It contains two |
| 207 | parts: period and acc_period. |
| 208 | When used for a line, period is the event count occurred when running |
| 209 | that line, acc_period is the accumulated event count occurred when |
| 210 | running that line and functions called by that line. Same thing applies |
| 211 | when it is used for a function, a source file, or a binary. |
| 212 | """ |
| 213 | def __init__(self, period=0, acc_period=0): |
| 214 | self.period = period |
| 215 | self.acc_period = acc_period |
| 216 | |
| 217 | |
| 218 | def __iadd__(self, other): |
| 219 | self.period += other.period |
| 220 | self.acc_period += other.acc_period |
| 221 | return self |
| 222 | |
| 223 | |
| 224 | class DsoPeriod(object): |
| 225 | """Period for each shared library""" |
| 226 | def __init__(self, dso_name): |
| 227 | self.dso_name = dso_name |
| 228 | self.period = Period() |
| 229 | |
| 230 | |
| 231 | def add_period(self, period): |
| 232 | self.period += period |
| 233 | |
| 234 | |
| 235 | class FilePeriod(object): |
| 236 | """Period for each source file""" |
| 237 | def __init__(self, file): |
| 238 | self.file = file |
| 239 | self.period = Period() |
| 240 | # Period for each line in the file. |
| 241 | self.line_dict = {} |
| 242 | # Period for each function in the source file. |
| 243 | self.function_dict = {} |
| 244 | |
| 245 | |
| 246 | def add_period(self, period): |
| 247 | self.period += period |
| 248 | |
| 249 | |
| 250 | def add_line_period(self, line, period): |
| 251 | a = self.line_dict.get(line) |
| 252 | if a is None: |
| 253 | self.line_dict[line] = a = Period() |
| 254 | a += period |
| 255 | |
| 256 | |
| 257 | def add_function_period(self, function_name, function_start_line, period): |
| 258 | a = self.function_dict.get(function_name) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 259 | if not a: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 260 | if function_start_line is None: |
| 261 | function_start_line = -1 |
| 262 | self.function_dict[function_name] = a = [function_start_line, Period()] |
| 263 | a[1] += period |
| 264 | |
| 265 | |
| 266 | class SourceFileAnnotator(object): |
| 267 | """group code for annotating source files""" |
| 268 | def __init__(self, config): |
| 269 | # check config variables |
| 270 | config_names = ['perf_data_list', 'symfs_dir', 'source_dirs', |
| 271 | 'annotate_dest_dir', 'comm_filters', 'pid_filters', |
| 272 | 'tid_filters', 'dso_filters', 'addr2line_path'] |
| 273 | for name in config_names: |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 274 | if name not in config: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 275 | log_fatal('config [%s] is missing' % name) |
| 276 | symfs_dir = config['symfs_dir'] |
| 277 | if symfs_dir and not os.path.isdir(symfs_dir): |
| 278 | log_fatal('[symfs_dir] "%s" is not a dir' % symfs_dir) |
| 279 | kallsyms = config['kallsyms'] |
| 280 | if kallsyms and not os.path.isfile(kallsyms): |
| 281 | log_fatal('[kallsyms] "%s" is not a file' % kallsyms) |
| 282 | source_dirs = config['source_dirs'] |
| 283 | for dir in source_dirs: |
| 284 | if not os.path.isdir(dir): |
| 285 | log_fatal('[source_dirs] "%s" is not a dir' % dir) |
| 286 | |
| 287 | # init member variables |
| 288 | self.config = config |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 289 | self.symfs_dir = config.get('symfs_dir') |
| 290 | self.kallsyms = config.get('kallsyms') |
| 291 | self.comm_filter = set(config['comm_filters']) if config.get('comm_filters') else None |
| 292 | if config.get('pid_filters'): |
| 293 | self.pid_filter = {int(x) for x in config['pid_filters']} |
| 294 | else: |
| 295 | self.pid_filter = None |
| 296 | if config.get('tid_filters'): |
| 297 | self.tid_filter = {int(x) for x in config['tid_filters']} |
| 298 | else: |
| 299 | self.tid_filter = None |
| 300 | self.dso_filter = set(config['dso_filters']) if config.get('dso_filters') else None |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 301 | |
| 302 | output_dir = config['annotate_dest_dir'] |
| 303 | if os.path.isdir(output_dir): |
| 304 | shutil.rmtree(output_dir) |
| 305 | os.makedirs(output_dir) |
| 306 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 307 | self.addr2line = Addr2Line(self.config['addr2line_path'], symfs_dir) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 308 | |
| 309 | |
| 310 | def annotate(self): |
| 311 | self._collect_addrs() |
| 312 | self._convert_addrs_to_lines() |
| 313 | self._generate_periods() |
| 314 | self._write_summary() |
| 315 | self._collect_source_files() |
| 316 | self._annotate_files() |
| 317 | |
| 318 | |
| 319 | def _collect_addrs(self): |
| 320 | """Read perf.data, collect all addresses we need to convert to |
| 321 | source file:line. |
| 322 | """ |
| 323 | for perf_data in self.config['perf_data_list']: |
| 324 | lib = ReportLib() |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 325 | lib.SetRecordFile(perf_data) |
| 326 | if self.symfs_dir: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 327 | lib.SetSymfs(self.symfs_dir) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 328 | if self.kallsyms: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 329 | lib.SetKallsymsFile(self.kallsyms) |
| 330 | while True: |
| 331 | sample = lib.GetNextSample() |
| 332 | if sample is None: |
| 333 | lib.Close() |
| 334 | break |
| 335 | if not self._filter_sample(sample): |
| 336 | continue |
| 337 | symbols = [] |
| 338 | symbols.append(lib.GetSymbolOfCurrentSample()) |
| 339 | callchain = lib.GetCallChainOfCurrentSample() |
| 340 | for i in range(callchain.nr): |
| 341 | symbols.append(callchain.entries[i].symbol) |
| 342 | for symbol in symbols: |
| 343 | if self._filter_symbol(symbol): |
| 344 | self.addr2line.add_addr(symbol.dso_name, symbol.vaddr_in_file) |
| 345 | self.addr2line.add_addr(symbol.dso_name, symbol.symbol_addr) |
| 346 | |
| 347 | |
| 348 | def _filter_sample(self, sample): |
| 349 | """Return true if the sample can be used.""" |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 350 | if self.comm_filter: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 351 | if sample.thread_comm not in self.comm_filter: |
| 352 | return False |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 353 | if self.pid_filter: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 354 | if sample.pid not in self.pid_filter: |
| 355 | return False |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 356 | if self.tid_filter: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 357 | if sample.tid not in self.tid_filter: |
| 358 | return False |
| 359 | return True |
| 360 | |
| 361 | |
| 362 | def _filter_symbol(self, symbol): |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 363 | if not self.dso_filter or symbol.dso_name in self.dso_filter: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 364 | return True |
| 365 | return False |
| 366 | |
| 367 | |
| 368 | def _convert_addrs_to_lines(self): |
| 369 | self.addr2line.convert_addrs_to_lines() |
| 370 | |
| 371 | |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 372 | def _generate_periods(self): |
| 373 | """read perf.data, collect Period for all types: |
| 374 | binaries, source files, functions, lines. |
| 375 | """ |
| 376 | self.period = 0 |
| 377 | self.dso_periods = dict() |
| 378 | self.file_periods = dict() |
| 379 | for perf_data in self.config['perf_data_list']: |
| 380 | lib = ReportLib() |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 381 | lib.SetRecordFile(perf_data) |
| 382 | if self.symfs_dir: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 383 | lib.SetSymfs(self.symfs_dir) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 384 | if self.kallsyms: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 385 | lib.SetKallsymsFile(self.kallsyms) |
| 386 | while True: |
| 387 | sample = lib.GetNextSample() |
| 388 | if sample is None: |
| 389 | lib.Close() |
| 390 | break |
| 391 | if not self._filter_sample(sample): |
| 392 | continue |
| 393 | symbols = [] |
| 394 | symbols.append(lib.GetSymbolOfCurrentSample()) |
| 395 | callchain = lib.GetCallChainOfCurrentSample() |
| 396 | for i in range(callchain.nr): |
| 397 | symbols.append(callchain.entries[i].symbol) |
| 398 | # Each sample has a callchain, but its period is only used once |
| 399 | # to add period for each function/source_line/source_file/binary. |
| 400 | # For example, if more than one entry in the callchain hits a |
| 401 | # function, the event count of that function is only increased once. |
| 402 | # Otherwise, we may get periods > 100%. |
| 403 | is_sample_used = False |
| 404 | used_dso_dict = dict() |
| 405 | used_file_dict = dict() |
| 406 | used_function_dict = dict() |
| 407 | used_line_dict = dict() |
| 408 | period = Period(sample.period, sample.period) |
| 409 | for i in range(len(symbols)): |
| 410 | symbol = symbols[i] |
| 411 | if i == 1: |
| 412 | period = Period(0, sample.period) |
| 413 | if not self._filter_symbol(symbol): |
| 414 | continue |
| 415 | is_sample_used = True |
| 416 | # Add period to dso. |
| 417 | self._add_dso_period(symbol.dso_name, period, used_dso_dict) |
| 418 | # Add period to source file. |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 419 | sources = self.addr2line.get_sources(symbol.dso_name, symbol.vaddr_in_file) |
| 420 | for source in sources: |
| 421 | if source.file: |
| 422 | self._add_file_period(source, period, used_file_dict) |
| 423 | # Add period to line. |
| 424 | if source.line: |
| 425 | self._add_line_period(source, period, used_line_dict) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 426 | # Add period to function. |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 427 | sources = self.addr2line.get_sources(symbol.dso_name, symbol.symbol_addr) |
| 428 | for source in sources: |
| 429 | if source.file: |
| 430 | self._add_file_period(source, period, used_file_dict) |
| 431 | if source.function: |
| 432 | self._add_function_period(source, period, used_function_dict) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 433 | |
| 434 | if is_sample_used: |
| 435 | self.period += sample.period |
| 436 | |
| 437 | |
| 438 | def _add_dso_period(self, dso_name, period, used_dso_dict): |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 439 | if dso_name not in used_dso_dict: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 440 | used_dso_dict[dso_name] = True |
| 441 | dso_period = self.dso_periods.get(dso_name) |
| 442 | if dso_period is None: |
| 443 | dso_period = self.dso_periods[dso_name] = DsoPeriod(dso_name) |
| 444 | dso_period.add_period(period) |
| 445 | |
| 446 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 447 | def _add_file_period(self, source, period, used_file_dict): |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 448 | if source.file_key not in used_file_dict: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 449 | used_file_dict[source.file_key] = True |
| 450 | file_period = self.file_periods.get(source.file) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 451 | if file_period is None: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 452 | file_period = self.file_periods[source.file] = FilePeriod(source.file) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 453 | file_period.add_period(period) |
| 454 | |
| 455 | |
| 456 | def _add_line_period(self, source, period, used_line_dict): |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 457 | if source.line_key not in used_line_dict: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 458 | used_line_dict[source.line_key] = True |
| 459 | file_period = self.file_periods[source.file] |
| 460 | file_period.add_line_period(source.line, period) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 461 | |
| 462 | |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 463 | def _add_function_period(self, source, period, used_function_dict): |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 464 | if source.function_key not in used_function_dict: |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 465 | used_function_dict[source.function_key] = True |
| 466 | file_period = self.file_periods[source.file] |
| 467 | file_period.add_function_period(source.function, source.line, period) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 468 | |
| 469 | |
| 470 | def _write_summary(self): |
| 471 | summary = os.path.join(self.config['annotate_dest_dir'], 'summary') |
| 472 | with open(summary, 'w') as f: |
| 473 | f.write('total period: %d\n\n' % self.period) |
| 474 | dso_periods = sorted(self.dso_periods.values(), |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 475 | key=lambda x: x.period.acc_period, reverse=True) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 476 | for dso_period in dso_periods: |
| 477 | f.write('dso %s: %s\n' % (dso_period.dso_name, |
| 478 | self._get_percentage_str(dso_period.period))) |
| 479 | f.write('\n') |
| 480 | |
| 481 | file_periods = sorted(self.file_periods.values(), |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 482 | key=lambda x: x.period.acc_period, reverse=True) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 483 | for file_period in file_periods: |
| 484 | f.write('file %s: %s\n' % (file_period.file, |
| 485 | self._get_percentage_str(file_period.period))) |
| 486 | for file_period in file_periods: |
| 487 | f.write('\n\n%s: %s\n' % (file_period.file, |
| 488 | self._get_percentage_str(file_period.period))) |
| 489 | values = [] |
| 490 | for func_name in file_period.function_dict.keys(): |
| 491 | func_start_line, period = file_period.function_dict[func_name] |
| 492 | values.append((func_name, func_start_line, period)) |
Yabin Cui | 98058a8 | 2017-05-08 14:23:19 -0700 | [diff] [blame] | 493 | values = sorted(values, key=lambda x: x[2].acc_period, reverse=True) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 494 | for value in values: |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 495 | f.write('\tfunction (%s): line %d, %s\n' % ( |
| 496 | value[0], value[1], self._get_percentage_str(value[2]))) |
| 497 | f.write('\n') |
| 498 | for line in sorted(file_period.line_dict.keys()): |
| 499 | f.write('\tline %d: %s\n' % ( |
| 500 | line, self._get_percentage_str(file_period.line_dict[line]))) |
| 501 | |
| 502 | |
| 503 | def _get_percentage_str(self, period, short=False): |
| 504 | s = 'acc_p: %f%%, p: %f%%' if short else 'accumulated_period: %f%%, period: %f%%' |
| 505 | return s % self._get_percentage(period) |
| 506 | |
| 507 | |
| 508 | def _get_percentage(self, period): |
| 509 | if self.period == 0: |
| 510 | return (0, 0) |
| 511 | acc_p = 100.0 * period.acc_period / self.period |
| 512 | p = 100.0 * period.period / self.period |
| 513 | return (acc_p, p) |
| 514 | |
| 515 | |
| 516 | def _collect_source_files(self): |
| 517 | self.source_file_dict = dict() |
| 518 | source_file_suffix = ['h', 'c', 'cpp', 'cc', 'java'] |
| 519 | for source_dir in self.config['source_dirs']: |
| 520 | for root, _, files in os.walk(source_dir): |
| 521 | for file in files: |
| 522 | if file[file.rfind('.')+1:] in source_file_suffix: |
| 523 | entry = self.source_file_dict.get(file) |
| 524 | if entry is None: |
| 525 | entry = self.source_file_dict[file] = [] |
| 526 | entry.append(os.path.join(root, file)) |
| 527 | |
| 528 | |
| 529 | def _find_source_file(self, file): |
| 530 | filename = file[file.rfind(os.sep)+1:] |
| 531 | source_files = self.source_file_dict.get(filename) |
| 532 | if source_files is None: |
| 533 | return None |
| 534 | match_count = 0 |
| 535 | result = None |
| 536 | for path in source_files: |
| 537 | if path.find(file) != -1: |
| 538 | match_count += 1 |
| 539 | result = path |
| 540 | if match_count > 1: |
| 541 | log_warning('multiple source for %s, select %s' % (file, result)) |
| 542 | return result |
| 543 | |
| 544 | |
| 545 | def _annotate_files(self): |
| 546 | """Annotate Source files: add acc_period/period for each source file. |
| 547 | 1. Annotate java source files, which have $JAVA_SRC_ROOT prefix. |
| 548 | 2. Annotate c++ source files. |
| 549 | """ |
| 550 | dest_dir = self.config['annotate_dest_dir'] |
| 551 | for key in self.file_periods.keys(): |
Yabin Cui | 5fac438 | 2017-01-06 12:47:31 -0800 | [diff] [blame] | 552 | is_java = False |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 553 | if key.startswith('$JAVA_SRC_ROOT/'): |
| 554 | path = key[len('$JAVA_SRC_ROOT/'):] |
| 555 | items = path.split('/') |
| 556 | path = os.sep.join(items) |
| 557 | from_path = self._find_source_file(path) |
| 558 | to_path = os.path.join(dest_dir, 'java', path) |
| 559 | is_java = True |
| 560 | elif key.startswith('/') and os.path.isfile(key): |
| 561 | path = key |
| 562 | from_path = path |
| 563 | to_path = os.path.join(dest_dir, path[1:]) |
Yabin Cui | cbddd38 | 2017-05-03 14:46:55 -0700 | [diff] [blame] | 564 | elif is_windows() and key.find(':\\') != -1 and os.path.isfile(key): |
| 565 | from_path = key |
| 566 | to_path = os.path.join(dest_dir, key.replace(':\\', '\\')) |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 567 | else: |
| 568 | path = key[1:] if key.startswith('/') else key |
| 569 | # Change path on device to path on host |
| 570 | path = os.sep.join(path.split('/')) |
| 571 | from_path = self._find_source_file(path) |
| 572 | to_path = os.path.join(dest_dir, path) |
| 573 | if from_path is None: |
| 574 | log_warning("can't find source file for path %s" % key) |
| 575 | continue |
| 576 | self._annotate_file(from_path, to_path, self.file_periods[key], is_java) |
| 577 | |
| 578 | |
| 579 | def _annotate_file(self, from_path, to_path, file_period, is_java): |
| 580 | """Annotate a source file. |
| 581 | |
| 582 | Annotate a source file in three steps: |
| 583 | 1. In the first line, show periods of this file. |
| 584 | 2. For each function, show periods of this function. |
| 585 | 3. For each line not hitting the same line as functions, show |
| 586 | line periods. |
| 587 | """ |
| 588 | log_info('annotate file %s' % from_path) |
| 589 | with open(from_path, 'r') as rf: |
| 590 | lines = rf.readlines() |
| 591 | |
| 592 | annotates = dict() |
| 593 | for line in file_period.line_dict.keys(): |
| 594 | annotates[line] = self._get_percentage_str(file_period.line_dict[line], True) |
| 595 | for func_name in file_period.function_dict.keys(): |
| 596 | func_start_line, period = file_period.function_dict[func_name] |
| 597 | if func_start_line == -1: |
| 598 | continue |
| 599 | line = func_start_line - 1 if is_java else func_start_line |
| 600 | annotates[line] = '[func] ' + self._get_percentage_str(period, True) |
| 601 | annotates[1] = '[file] ' + self._get_percentage_str(file_period.period, True) |
| 602 | |
| 603 | max_annotate_cols = 0 |
| 604 | for key in annotates.keys(): |
| 605 | max_annotate_cols = max(max_annotate_cols, len(annotates[key])) |
| 606 | |
| 607 | empty_annotate = ' ' * (max_annotate_cols + 6) |
| 608 | |
| 609 | dirname = os.path.dirname(to_path) |
| 610 | if not os.path.isdir(dirname): |
| 611 | os.makedirs(dirname) |
| 612 | with open(to_path, 'w') as wf: |
| 613 | for line in range(1, len(lines) + 1): |
| 614 | annotate = annotates.get(line) |
| 615 | if annotate is None: |
Yabin Cui | 06956f3 | 2017-04-28 10:44:16 -0700 | [diff] [blame] | 616 | if not lines[line-1].strip(): |
| 617 | annotate = '' |
| 618 | else: |
| 619 | annotate = empty_annotate |
Yabin Cui | d09d15b | 2016-12-12 18:18:07 -0800 | [diff] [blame] | 620 | else: |
| 621 | annotate = '/* ' + annotate + ( |
| 622 | ' ' * (max_annotate_cols - len(annotate))) + ' */' |
| 623 | wf.write(annotate) |
| 624 | wf.write(lines[line-1]) |
| 625 | |
| 626 | |
| 627 | if __name__ == '__main__': |
| 628 | parser = argparse.ArgumentParser( |
| 629 | description='Annotate based on perf.data. See configurations in annotate.config.') |
| 630 | parser.add_argument('--config', default='annotate.config', |
| 631 | help='Set configuration file. Default is annotate.config.') |
| 632 | args = parser.parse_args() |
| 633 | config = load_config(args.config) |
| 634 | annotator = SourceFileAnnotator(config) |
Yabin Cui | 129b5d2 | 2017-03-16 13:00:43 -0700 | [diff] [blame] | 635 | annotator.annotate() |
Yabin Cui | cbddd38 | 2017-05-03 14:46:55 -0700 | [diff] [blame] | 636 | log_info('annotate finish successfully, please check result in annotated_files/.') |