• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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('>', '&gt;').replace('<', '&lt;')
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