1#!/usr/bin/env python 2# 3# Copyright (C) 2017 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 18import argparse 19import collections 20import datetime 21import json 22import os 23import sys 24 25from simpleperf_report_lib import ReportLib 26from utils import log_info, log_exit 27from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser 28from utils import SourceFileSearcher 29 30MAX_CALLSTACK_LENGTH = 750 31 32class HtmlWriter(object): 33 34 def __init__(self, output_path): 35 self.fh = open(output_path, 'w') 36 self.tag_stack = [] 37 38 def close(self): 39 self.fh.close() 40 41 def open_tag(self, tag, **attrs): 42 attr_str = '' 43 for key in attrs: 44 attr_str += ' %s="%s"' % (key, attrs[key]) 45 self.fh.write('<%s%s>' % (tag, attr_str)) 46 self.tag_stack.append(tag) 47 return self 48 49 def close_tag(self, tag=None): 50 if tag: 51 assert tag == self.tag_stack[-1] 52 self.fh.write('</%s>\n' % self.tag_stack.pop()) 53 54 def add(self, text): 55 self.fh.write(text) 56 return self 57 58 def add_file(self, file_path): 59 file_path = os.path.join(get_script_dir(), file_path) 60 with open(file_path, 'r') as f: 61 self.add(f.read()) 62 return self 63 64def modify_text_for_html(text): 65 return text.replace('>', '>').replace('<', '<') 66 67class EventScope(object): 68 69 def __init__(self, name): 70 self.name = name 71 self.processes = {} # map from pid to ProcessScope 72 self.sample_count = 0 73 self.event_count = 0 74 75 def get_process(self, pid): 76 process = self.processes.get(pid) 77 if not process: 78 process = self.processes[pid] = ProcessScope(pid) 79 return process 80 81 def get_sample_info(self, gen_addr_hit_map): 82 result = {} 83 result['eventName'] = self.name 84 result['eventCount'] = self.event_count 85 processes = sorted(self.processes.values(), key=lambda a: a.event_count, reverse=True) 86 result['processes'] = [process.get_sample_info(gen_addr_hit_map) 87 for process in processes] 88 return result 89 90 @property 91 def threads(self): 92 for process in self.processes.values(): 93 for thread in process.threads.values(): 94 yield thread 95 96 @property 97 def libraries(self): 98 for process in self.processes.values(): 99 for thread in process.threads.values(): 100 for lib in thread.libs.values(): 101 yield lib 102 103 104class ProcessScope(object): 105 106 def __init__(self, pid): 107 self.pid = pid 108 self.name = '' 109 self.event_count = 0 110 self.threads = {} # map from tid to ThreadScope 111 112 def get_thread(self, tid, thread_name): 113 thread = self.threads.get(tid) 114 if not thread: 115 thread = self.threads[tid] = ThreadScope(tid) 116 thread.name = thread_name 117 if self.pid == tid: 118 self.name = thread_name 119 return thread 120 121 def get_sample_info(self, gen_addr_hit_map): 122 result = {} 123 result['pid'] = self.pid 124 result['eventCount'] = self.event_count 125 threads = sorted(self.threads.values(), key=lambda a: a.event_count, reverse=True) 126 result['threads'] = [thread.get_sample_info(gen_addr_hit_map) 127 for thread in threads] 128 return result 129 130 131class ThreadScope(object): 132 133 def __init__(self, tid): 134 self.tid = tid 135 self.name = '' 136 self.event_count = 0 137 self.sample_count = 0 138 self.libs = {} # map from lib_id to LibScope 139 self.call_graph = CallNode(-1) 140 self.reverse_call_graph = CallNode(-1) 141 142 def add_callstack(self, event_count, callstack, build_addr_hit_map): 143 """ callstack is a list of tuple (lib_id, func_id, addr). 144 For each i > 0, callstack[i] calls callstack[i-1].""" 145 hit_func_ids = set() 146 for i, (lib_id, func_id, addr) in enumerate(callstack): 147 # When a callstack contains recursive function, only add for each function once. 148 if func_id in hit_func_ids: 149 continue 150 hit_func_ids.add(func_id) 151 152 lib = self.libs.get(lib_id) 153 if not lib: 154 lib = self.libs[lib_id] = LibScope(lib_id) 155 function = lib.get_function(func_id) 156 function.subtree_event_count += event_count 157 if i == 0: 158 lib.event_count += event_count 159 function.event_count += event_count 160 function.sample_count += 1 161 if build_addr_hit_map: 162 function.build_addr_hit_map(addr, event_count if i == 0 else 0, event_count) 163 164 # build call graph and reverse call graph 165 node = self.call_graph 166 for item in reversed(callstack): 167 node = node.get_child(item[1]) 168 node.event_count += event_count 169 node = self.reverse_call_graph 170 for item in callstack: 171 node = node.get_child(item[1]) 172 node.event_count += event_count 173 174 def update_subtree_event_count(self): 175 self.call_graph.update_subtree_event_count() 176 self.reverse_call_graph.update_subtree_event_count() 177 178 def limit_percents(self, min_func_limit, min_callchain_percent, hit_func_ids): 179 for lib in self.libs.values(): 180 to_del_funcs = [] 181 for function in lib.functions.values(): 182 if function.subtree_event_count < min_func_limit: 183 to_del_funcs.append(function.func_id) 184 else: 185 hit_func_ids.add(function.func_id) 186 for func_id in to_del_funcs: 187 del lib.functions[func_id] 188 min_limit = min_callchain_percent * 0.01 * self.call_graph.subtree_event_count 189 self.call_graph.cut_edge(min_limit, hit_func_ids) 190 self.reverse_call_graph.cut_edge(min_limit, hit_func_ids) 191 192 def get_sample_info(self, gen_addr_hit_map): 193 result = {} 194 result['tid'] = self.tid 195 result['eventCount'] = self.event_count 196 result['sampleCount'] = self.sample_count 197 result['libs'] = [lib.gen_sample_info(gen_addr_hit_map) 198 for lib in self.libs.values()] 199 result['g'] = self.call_graph.gen_sample_info() 200 result['rg'] = self.reverse_call_graph.gen_sample_info() 201 return result 202 203 204class LibScope(object): 205 206 def __init__(self, lib_id): 207 self.lib_id = lib_id 208 self.event_count = 0 209 self.functions = {} # map from func_id to FunctionScope. 210 211 def get_function(self, func_id): 212 function = self.functions.get(func_id) 213 if not function: 214 function = self.functions[func_id] = FunctionScope(func_id) 215 return function 216 217 def gen_sample_info(self, gen_addr_hit_map): 218 result = {} 219 result['libId'] = self.lib_id 220 result['eventCount'] = self.event_count 221 result['functions'] = [func.gen_sample_info(gen_addr_hit_map) 222 for func in self.functions.values()] 223 return result 224 225 226class FunctionScope(object): 227 228 def __init__(self, func_id): 229 self.func_id = func_id 230 self.sample_count = 0 231 self.event_count = 0 232 self.subtree_event_count = 0 233 self.addr_hit_map = None # map from addr to [event_count, subtree_event_count]. 234 # map from (source_file_id, line) to [event_count, subtree_event_count]. 235 self.line_hit_map = None 236 237 def build_addr_hit_map(self, addr, event_count, subtree_event_count): 238 if self.addr_hit_map is None: 239 self.addr_hit_map = {} 240 count_info = self.addr_hit_map.get(addr) 241 if count_info is None: 242 self.addr_hit_map[addr] = [event_count, subtree_event_count] 243 else: 244 count_info[0] += event_count 245 count_info[1] += subtree_event_count 246 247 def build_line_hit_map(self, source_file_id, line, event_count, subtree_event_count): 248 if self.line_hit_map is None: 249 self.line_hit_map = {} 250 key = (source_file_id, line) 251 count_info = self.line_hit_map.get(key) 252 if count_info is None: 253 self.line_hit_map[key] = [event_count, subtree_event_count] 254 else: 255 count_info[0] += event_count 256 count_info[1] += subtree_event_count 257 258 def gen_sample_info(self, gen_addr_hit_map): 259 result = {} 260 result['f'] = self.func_id 261 result['c'] = [self.sample_count, self.event_count, self.subtree_event_count] 262 if self.line_hit_map: 263 items = [] 264 for key in self.line_hit_map: 265 count_info = self.line_hit_map[key] 266 item = {'f': key[0], 'l': key[1], 'e': count_info[0], 's': count_info[1]} 267 items.append(item) 268 result['s'] = items 269 if gen_addr_hit_map and self.addr_hit_map: 270 items = [] 271 for addr in sorted(self.addr_hit_map): 272 count_info = self.addr_hit_map[addr] 273 items.append({'a': addr, 'e': count_info[0], 's': count_info[1]}) 274 result['a'] = items 275 return result 276 277 278class CallNode(object): 279 280 def __init__(self, func_id): 281 self.event_count = 0 282 self.subtree_event_count = 0 283 self.func_id = func_id 284 self.children = collections.OrderedDict() # map from func_id to CallNode 285 286 def get_child(self, func_id): 287 child = self.children.get(func_id) 288 if not child: 289 child = self.children[func_id] = CallNode(func_id) 290 return child 291 292 def update_subtree_event_count(self): 293 self.subtree_event_count = self.event_count 294 for child in self.children.values(): 295 self.subtree_event_count += child.update_subtree_event_count() 296 return self.subtree_event_count 297 298 def cut_edge(self, min_limit, hit_func_ids): 299 hit_func_ids.add(self.func_id) 300 to_del_children = [] 301 for key in self.children: 302 child = self.children[key] 303 if child.subtree_event_count < min_limit: 304 to_del_children.append(key) 305 else: 306 child.cut_edge(min_limit, hit_func_ids) 307 for key in to_del_children: 308 del self.children[key] 309 310 def gen_sample_info(self): 311 result = {} 312 result['e'] = self.event_count 313 result['s'] = self.subtree_event_count 314 result['f'] = self.func_id 315 result['c'] = [child.gen_sample_info() for child in self.children.values()] 316 return result 317 318 319class LibSet(object): 320 """ Collection of shared libraries used in perf.data. """ 321 def __init__(self): 322 self.lib_name_to_id = {} 323 self.lib_id_to_name = [] 324 325 def get_lib_id(self, lib_name): 326 lib_id = self.lib_name_to_id.get(lib_name) 327 if lib_id is None: 328 lib_id = len(self.lib_id_to_name) 329 self.lib_name_to_id[lib_name] = lib_id 330 self.lib_id_to_name.append(lib_name) 331 return lib_id 332 333 def get_lib_name(self, lib_id): 334 return self.lib_id_to_name[lib_id] 335 336 337class Function(object): 338 """ Represent a function in a shared library. """ 339 def __init__(self, lib_id, func_name, func_id, start_addr, addr_len): 340 self.lib_id = lib_id 341 self.func_name = func_name 342 self.func_id = func_id 343 self.start_addr = start_addr 344 self.addr_len = addr_len 345 self.source_info = None 346 self.disassembly = None 347 348 349class FunctionSet(object): 350 """ Collection of functions used in perf.data. """ 351 def __init__(self): 352 self.name_to_func = {} 353 self.id_to_func = {} 354 355 def get_func_id(self, lib_id, symbol): 356 key = (lib_id, symbol.symbol_name) 357 function = self.name_to_func.get(key) 358 if function is None: 359 func_id = len(self.id_to_func) 360 function = Function(lib_id, symbol.symbol_name, func_id, symbol.symbol_addr, 361 symbol.symbol_len) 362 self.name_to_func[key] = function 363 self.id_to_func[func_id] = function 364 return function.func_id 365 366 def trim_functions(self, left_func_ids): 367 """ Remove functions excepts those in left_func_ids. """ 368 for function in self.name_to_func.values(): 369 if function.func_id not in left_func_ids: 370 del self.id_to_func[function.func_id] 371 # name_to_func will not be used. 372 self.name_to_func = None 373 374 375class SourceFile(object): 376 """ A source file containing source code hit by samples. """ 377 def __init__(self, file_id, abstract_path): 378 self.file_id = file_id 379 self.abstract_path = abstract_path # path reported by addr2line 380 self.real_path = None # file path in the file system 381 self.requested_lines = set() 382 self.line_to_code = {} # map from line to code in that line. 383 384 def request_lines(self, start_line, end_line): 385 self.requested_lines |= set(range(start_line, end_line + 1)) 386 387 def add_source_code(self, real_path): 388 self.real_path = real_path 389 with open(real_path, 'r') as f: 390 source_code = f.readlines() 391 max_line = len(source_code) 392 for line in self.requested_lines: 393 if line > 0 and line <= max_line: 394 self.line_to_code[line] = source_code[line - 1] 395 # requested_lines is no longer used. 396 self.requested_lines = None 397 398 399class SourceFileSet(object): 400 """ Collection of source files. """ 401 def __init__(self): 402 self.path_to_source_files = {} # map from file path to SourceFile. 403 404 def get_source_file(self, file_path): 405 source_file = self.path_to_source_files.get(file_path) 406 if source_file is None: 407 source_file = SourceFile(len(self.path_to_source_files), file_path) 408 self.path_to_source_files[file_path] = source_file 409 return source_file 410 411 def load_source_code(self, source_dirs): 412 file_searcher = SourceFileSearcher(source_dirs) 413 for source_file in self.path_to_source_files.values(): 414 real_path = file_searcher.get_real_path(source_file.abstract_path) 415 if real_path: 416 source_file.add_source_code(real_path) 417 418 419 420class RecordData(object): 421 422 """RecordData reads perf.data, and generates data used by report.js in json format. 423 All generated items are listed as below: 424 1. recordTime: string 425 2. machineType: string 426 3. androidVersion: string 427 4. recordCmdline: string 428 5. totalSamples: int 429 6. processNames: map from pid to processName. 430 7. threadNames: map from tid to threadName. 431 8. libList: an array of libNames, indexed by libId. 432 9. functionMap: map from functionId to funcData. 433 funcData = { 434 l: libId 435 f: functionName 436 s: [sourceFileId, startLine, endLine] [optional] 437 d: [(disassembly, addr)] [optional] 438 } 439 440 10. sampleInfo = [eventInfo] 441 eventInfo = { 442 eventName 443 eventCount 444 processes: [processInfo] 445 } 446 processInfo = { 447 pid 448 eventCount 449 threads: [threadInfo] 450 } 451 threadInfo = { 452 tid 453 eventCount 454 sampleCount 455 libs: [libInfo], 456 g: callGraph, 457 rg: reverseCallgraph 458 } 459 libInfo = { 460 libId, 461 eventCount, 462 functions: [funcInfo] 463 } 464 funcInfo = { 465 f: functionId 466 c: [sampleCount, eventCount, subTreeEventCount] 467 s: [sourceCodeInfo] [optional] 468 a: [addrInfo] (sorted by addrInfo.addr) [optional] 469 } 470 callGraph and reverseCallGraph are both of type CallNode. 471 callGraph shows how a function calls other functions. 472 reverseCallGraph shows how a function is called by other functions. 473 CallNode { 474 e: selfEventCount 475 s: subTreeEventCount 476 f: functionId 477 c: [CallNode] # children 478 } 479 480 sourceCodeInfo { 481 f: sourceFileId 482 l: line 483 e: eventCount 484 s: subtreeEventCount 485 } 486 487 addrInfo { 488 a: addr 489 e: eventCount 490 s: subtreeEventCount 491 } 492 493 11. sourceFiles: an array of sourceFile, indexed by sourceFileId. 494 sourceFile { 495 path 496 code: # a map from line to code for that line. 497 } 498 """ 499 500 def __init__(self, binary_cache_path, ndk_path, build_addr_hit_map): 501 self.binary_cache_path = binary_cache_path 502 self.ndk_path = ndk_path 503 self.build_addr_hit_map = build_addr_hit_map 504 self.meta_info = None 505 self.cmdline = None 506 self.arch = None 507 self.events = {} 508 self.libs = LibSet() 509 self.functions = FunctionSet() 510 self.total_samples = 0 511 self.source_files = SourceFileSet() 512 self.gen_addr_hit_map_in_record_info = False 513 514 def load_record_file(self, record_file, show_art_frames): 515 lib = ReportLib() 516 lib.SetRecordFile(record_file) 517 # If not showing ip for unknown symbols, the percent of the unknown symbol may be 518 # accumulated to very big, and ranks first in the sample table. 519 lib.ShowIpForUnknownSymbol() 520 if show_art_frames: 521 lib.ShowArtFrames() 522 if self.binary_cache_path: 523 lib.SetSymfs(self.binary_cache_path) 524 self.meta_info = lib.MetaInfo() 525 self.cmdline = lib.GetRecordCmd() 526 self.arch = lib.GetArch() 527 while True: 528 raw_sample = lib.GetNextSample() 529 if not raw_sample: 530 lib.Close() 531 break 532 raw_event = lib.GetEventOfCurrentSample() 533 symbol = lib.GetSymbolOfCurrentSample() 534 callchain = lib.GetCallChainOfCurrentSample() 535 event = self._get_event(raw_event.name) 536 self.total_samples += 1 537 event.sample_count += 1 538 event.event_count += raw_sample.period 539 process = event.get_process(raw_sample.pid) 540 process.event_count += raw_sample.period 541 thread = process.get_thread(raw_sample.tid, raw_sample.thread_comm) 542 thread.event_count += raw_sample.period 543 thread.sample_count += 1 544 545 lib_id = self.libs.get_lib_id(symbol.dso_name) 546 func_id = self.functions.get_func_id(lib_id, symbol) 547 callstack = [(lib_id, func_id, symbol.vaddr_in_file)] 548 for i in range(callchain.nr): 549 symbol = callchain.entries[i].symbol 550 lib_id = self.libs.get_lib_id(symbol.dso_name) 551 func_id = self.functions.get_func_id(lib_id, symbol) 552 callstack.append((lib_id, func_id, symbol.vaddr_in_file)) 553 if len(callstack) > MAX_CALLSTACK_LENGTH: 554 callstack = callstack[:MAX_CALLSTACK_LENGTH] 555 thread.add_callstack(raw_sample.period, callstack, self.build_addr_hit_map) 556 557 for event in self.events.values(): 558 for thread in event.threads: 559 thread.update_subtree_event_count() 560 561 def limit_percents(self, min_func_percent, min_callchain_percent): 562 hit_func_ids = set() 563 for event in self.events.values(): 564 min_limit = event.event_count * min_func_percent * 0.01 565 for process in event.processes.values(): 566 to_del_threads = [] 567 for thread in process.threads.values(): 568 if thread.call_graph.subtree_event_count < min_limit: 569 to_del_threads.append(thread.tid) 570 else: 571 thread.limit_percents(min_limit, min_callchain_percent, hit_func_ids) 572 for thread in to_del_threads: 573 del process.threads[thread] 574 self.functions.trim_functions(hit_func_ids) 575 576 def _get_event(self, event_name): 577 if event_name not in self.events: 578 self.events[event_name] = EventScope(event_name) 579 return self.events[event_name] 580 581 def add_source_code(self, source_dirs, filter_lib): 582 """ Collect source code information: 583 1. Find line ranges for each function in FunctionSet. 584 2. Find line for each addr in FunctionScope.addr_hit_map. 585 3. Collect needed source code in SourceFileSet. 586 """ 587 addr2line = Addr2Nearestline(self.ndk_path, self.binary_cache_path, False) 588 # Request line range for each function. 589 for function in self.functions.id_to_func.values(): 590 if function.func_name == 'unknown': 591 continue 592 lib_name = self.libs.get_lib_name(function.lib_id) 593 if filter_lib(lib_name): 594 addr2line.add_addr(lib_name, function.start_addr, function.start_addr) 595 addr2line.add_addr(lib_name, function.start_addr, 596 function.start_addr + function.addr_len - 1) 597 # Request line for each addr in FunctionScope.addr_hit_map. 598 for event in self.events.values(): 599 for lib in event.libraries: 600 lib_name = self.libs.get_lib_name(lib.lib_id) 601 if filter_lib(lib_name): 602 for function in lib.functions.values(): 603 func_addr = self.functions.id_to_func[function.func_id].start_addr 604 for addr in function.addr_hit_map: 605 addr2line.add_addr(lib_name, func_addr, addr) 606 addr2line.convert_addrs_to_lines() 607 608 # Set line range for each function. 609 for function in self.functions.id_to_func.values(): 610 if function.func_name == 'unknown': 611 continue 612 dso = addr2line.get_dso(self.libs.get_lib_name(function.lib_id)) 613 if not dso: 614 continue 615 start_source = addr2line.get_addr_source(dso, function.start_addr) 616 end_source = addr2line.get_addr_source(dso, function.start_addr + function.addr_len - 1) 617 if not start_source or not end_source: 618 continue 619 start_file_path, start_line = start_source[-1] 620 end_file_path, end_line = end_source[-1] 621 if start_file_path != end_file_path or start_line > end_line: 622 continue 623 source_file = self.source_files.get_source_file(start_file_path) 624 source_file.request_lines(start_line, end_line) 625 function.source_info = (source_file.file_id, start_line, end_line) 626 627 # Build FunctionScope.line_hit_map. 628 for event in self.events.values(): 629 for lib in event.libraries: 630 dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id)) 631 if not dso: 632 continue 633 for function in lib.functions.values(): 634 for addr in function.addr_hit_map: 635 source = addr2line.get_addr_source(dso, addr) 636 if not source: 637 continue 638 for file_path, line in source: 639 source_file = self.source_files.get_source_file(file_path) 640 # Show [line - 5, line + 5] of the line hit by a sample. 641 source_file.request_lines(line - 5, line + 5) 642 count_info = function.addr_hit_map[addr] 643 function.build_line_hit_map(source_file.file_id, line, count_info[0], 644 count_info[1]) 645 646 # Collect needed source code in SourceFileSet. 647 self.source_files.load_source_code(source_dirs) 648 649 def add_disassembly(self, filter_lib): 650 """ Collect disassembly information: 651 1. Use objdump to collect disassembly for each function in FunctionSet. 652 2. Set flag to dump addr_hit_map when generating record info. 653 """ 654 objdump = Objdump(self.ndk_path, self.binary_cache_path) 655 cur_lib_name = None 656 dso_info = None 657 for function in sorted(self.functions.id_to_func.values(), key=lambda a: a.lib_id): 658 if function.func_name == 'unknown': 659 continue 660 lib_name = self.libs.get_lib_name(function.lib_id) 661 if lib_name != cur_lib_name: 662 cur_lib_name = lib_name 663 if filter_lib(lib_name): 664 dso_info = objdump.get_dso_info(lib_name) 665 else: 666 dso_info = None 667 if dso_info: 668 log_info('Disassemble %s' % dso_info[0]) 669 if dso_info: 670 code = objdump.disassemble_code(dso_info, function.start_addr, function.addr_len) 671 function.disassembly = code 672 673 self.gen_addr_hit_map_in_record_info = True 674 675 def gen_record_info(self): 676 record_info = {} 677 timestamp = self.meta_info.get('timestamp') 678 if timestamp: 679 t = datetime.datetime.fromtimestamp(int(timestamp)) 680 else: 681 t = datetime.datetime.now() 682 record_info['recordTime'] = t.strftime('%Y-%m-%d (%A) %H:%M:%S') 683 684 product_props = self.meta_info.get('product_props') 685 machine_type = self.arch 686 if product_props: 687 manufacturer, model, name = product_props.split(':') 688 machine_type = '%s (%s) by %s, arch %s' % (model, name, manufacturer, self.arch) 689 record_info['machineType'] = machine_type 690 record_info['androidVersion'] = self.meta_info.get('android_version', '') 691 record_info['recordCmdline'] = self.cmdline 692 record_info['totalSamples'] = self.total_samples 693 record_info['processNames'] = self._gen_process_names() 694 record_info['threadNames'] = self._gen_thread_names() 695 record_info['libList'] = self._gen_lib_list() 696 record_info['functionMap'] = self._gen_function_map() 697 record_info['sampleInfo'] = self._gen_sample_info() 698 record_info['sourceFiles'] = self._gen_source_files() 699 return record_info 700 701 def _gen_process_names(self): 702 process_names = {} 703 for event in self.events.values(): 704 for process in event.processes.values(): 705 process_names[process.pid] = process.name 706 return process_names 707 708 def _gen_thread_names(self): 709 thread_names = {} 710 for event in self.events.values(): 711 for process in event.processes.values(): 712 for thread in process.threads.values(): 713 thread_names[thread.tid] = thread.name 714 return thread_names 715 716 def _gen_lib_list(self): 717 return [modify_text_for_html(x) for x in self.libs.lib_id_to_name] 718 719 def _gen_function_map(self): 720 func_map = {} 721 for func_id in sorted(self.functions.id_to_func): 722 function = self.functions.id_to_func[func_id] 723 func_data = {} 724 func_data['l'] = function.lib_id 725 func_data['f'] = modify_text_for_html(function.func_name) 726 if function.source_info: 727 func_data['s'] = function.source_info 728 if function.disassembly: 729 disassembly_list = [] 730 for code, addr in function.disassembly: 731 disassembly_list.append([modify_text_for_html(code), addr]) 732 func_data['d'] = disassembly_list 733 func_map[func_id] = func_data 734 return func_map 735 736 def _gen_sample_info(self): 737 return [event.get_sample_info(self.gen_addr_hit_map_in_record_info) 738 for event in self.events.values()] 739 740 def _gen_source_files(self): 741 source_files = sorted(self.source_files.path_to_source_files.values(), 742 key=lambda x: x.file_id) 743 file_list = [] 744 for source_file in source_files: 745 file_data = {} 746 if not source_file.real_path: 747 file_data['path'] = '' 748 file_data['code'] = {} 749 else: 750 file_data['path'] = source_file.real_path 751 code_map = {} 752 for line in source_file.line_to_code: 753 code_map[line] = modify_text_for_html(source_file.line_to_code[line]) 754 file_data['code'] = code_map 755 file_list.append(file_data) 756 return file_list 757 758URLS = { 759 'jquery': 'https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js', 760 'bootstrap4-css': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css', 761 'bootstrap4-popper': 762 'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js', 763 'bootstrap4': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js', 764 'dataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js', 765 'dataTable-bootstrap4': 'https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js', 766 'dataTable-css': 'https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css', 767 'gstatic-charts': 'https://www.gstatic.com/charts/loader.js', 768} 769 770class ReportGenerator(object): 771 772 def __init__(self, html_path): 773 self.hw = HtmlWriter(html_path) 774 self.hw.open_tag('html') 775 self.hw.open_tag('head') 776 for css in ['bootstrap4-css', 'dataTable-css']: 777 self.hw.open_tag('link', rel='stylesheet', type='text/css', href=URLS[css]).close_tag() 778 for js in ['jquery', 'bootstrap4-popper', 'bootstrap4', 'dataTable', 'dataTable-bootstrap4', 779 'gstatic-charts']: 780 self.hw.open_tag('script', src=URLS[js]).close_tag() 781 782 self.hw.open_tag('script').add( 783 "google.charts.load('current', {'packages': ['corechart', 'table']});").close_tag() 784 self.hw.open_tag('style', type='text/css').add(""" 785 .colForLine { width: 50px; } 786 .colForCount { width: 100px; } 787 .tableCell { font-size: 17px; } 788 .boldTableCell { font-weight: bold; font-size: 17px; } 789 """).close_tag() 790 self.hw.close_tag('head') 791 self.hw.open_tag('body') 792 self.record_info = {} 793 794 def write_content_div(self): 795 self.hw.open_tag('div', id='report_content').close_tag() 796 797 def write_record_data(self, record_data): 798 self.hw.open_tag('script', id='record_data', type='application/json') 799 self.hw.add(json.dumps(record_data)) 800 self.hw.close_tag() 801 802 def write_script(self): 803 self.hw.open_tag('script').add_file('report_html.js').close_tag() 804 805 def finish(self): 806 self.hw.close_tag('body') 807 self.hw.close_tag('html') 808 self.hw.close() 809 810 811def main(): 812 sys.setrecursionlimit(MAX_CALLSTACK_LENGTH * 2 + 50) 813 parser = argparse.ArgumentParser(description='report profiling data') 814 parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help=""" 815 Set profiling data file to report. Default is perf.data.""") 816 parser.add_argument('-o', '--report_path', default='report.html', help=""" 817 Set output html file. Default is report.html.""") 818 parser.add_argument('--min_func_percent', default=0.01, type=float, help=""" 819 Set min percentage of functions shown in the report. 820 For example, when set to 0.01, only functions taking >= 0.01%% of total 821 event count are collected in the report. Default is 0.01.""") 822 parser.add_argument('--min_callchain_percent', default=0.01, type=float, help=""" 823 Set min percentage of callchains shown in the report. 824 It is used to limit nodes shown in the function flamegraph. For example, 825 when set to 0.01, only callchains taking >= 0.01%% of the event count of 826 the starting function are collected in the report. Default is 0.01.""") 827 parser.add_argument('--add_source_code', action='store_true', help='Add source code.') 828 parser.add_argument('--source_dirs', nargs='+', help='Source code directories.') 829 parser.add_argument('--add_disassembly', action='store_true', help='Add disassembled code.') 830 parser.add_argument('--binary_filter', nargs='+', help="""Annotate source code and disassembly 831 only for selected binaries.""") 832 parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.') 833 parser.add_argument('--no_browser', action='store_true', help="Don't open report in browser.") 834 parser.add_argument('--show_art_frames', action='store_true', 835 help='Show frames of internal methods in the ART Java interpreter.') 836 args = parser.parse_args() 837 838 # 1. Process args. 839 binary_cache_path = 'binary_cache' 840 if not os.path.isdir(binary_cache_path): 841 if args.add_source_code or args.add_disassembly: 842 log_exit("""binary_cache/ doesn't exist. Can't add source code or disassembled code 843 without collected binaries. Please run binary_cache_builder.py to 844 collect binaries for current profiling data, or run app_profiler.py 845 without -nb option.""") 846 binary_cache_path = None 847 848 if args.add_source_code and not args.source_dirs: 849 log_exit('--source_dirs is needed to add source code.') 850 build_addr_hit_map = args.add_source_code or args.add_disassembly 851 ndk_path = None if not args.ndk_path else args.ndk_path[0] 852 853 # 2. Produce record data. 854 record_data = RecordData(binary_cache_path, ndk_path, build_addr_hit_map) 855 for record_file in args.record_file: 856 record_data.load_record_file(record_file, args.show_art_frames) 857 record_data.limit_percents(args.min_func_percent, args.min_callchain_percent) 858 859 def filter_lib(lib_name): 860 if not args.binary_filter: 861 return True 862 for binary in args.binary_filter: 863 if binary in lib_name: 864 return True 865 return False 866 if args.add_source_code: 867 record_data.add_source_code(args.source_dirs, filter_lib) 868 if args.add_disassembly: 869 record_data.add_disassembly(filter_lib) 870 871 # 3. Generate report html. 872 report_generator = ReportGenerator(args.report_path) 873 report_generator.write_script() 874 report_generator.write_content_div() 875 report_generator.write_record_data(record_data.gen_record_info()) 876 report_generator.finish() 877 878 if not args.no_browser: 879 open_report_in_browser(args.report_path) 880 log_info("Report generated at '%s'." % args.report_path) 881 882 883if __name__ == '__main__': 884 main() 885