|  | #!/usr/bin/env python | 
|  | # | 
|  | # Copyright (C) 2017 The Android Open Source Project | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | # | 
|  |  | 
|  | """debug_unwind_reporter.py: report dwarf unwinding results generated by debug-unwind cmd. | 
|  | Below is an example using debug_unwind_reporter.py: | 
|  | 1. Record with "-g --no-unwind" option on device. | 
|  | simpleperf record -g --no-unwind --app com.google.sample.tunnel --duration 10 | 
|  | 2. Use debug-unwind cmd to unwind samples in perf.data on device. | 
|  | simpleperf debug-unwind | 
|  | 3. Pull perf.data.debug on host, and report it with this script. | 
|  | python debug_unwind_reporter.py | 
|  |  | 
|  | It reports below items: | 
|  | 1. time used for offline dwarf unwinding for each sample. | 
|  | 2. mem consumption before and after unwinding samples. | 
|  | 3. For samples that don't have complete callchains, report their regs, stack data, | 
|  | unwinding failure info, callchains. Samples having the same failure reason | 
|  | in the same function are only reported once. | 
|  |  | 
|  | It can be used to: | 
|  | 1. show unwinding failures. | 
|  | 2. show performance and memory consumption change caused by simpleperf/unwinder changes. | 
|  | """ | 
|  |  | 
|  | from __future__ import print_function | 
|  | import argparse | 
|  | import bisect | 
|  | import collections | 
|  | import copy | 
|  | import re | 
|  | import subprocess | 
|  |  | 
|  | from utils import bytes_to_str, log_exit, log_fatal, get_host_binary_path | 
|  |  | 
|  |  | 
|  | class MapEntry(object): | 
|  |  | 
|  | def __init__(self, start, end, filename): | 
|  | self.start = start | 
|  | self.end = end | 
|  | self.filename = filename | 
|  |  | 
|  | def __lt__(self, other): | 
|  | return self.start < other.start | 
|  |  | 
|  | class ProcessMaps(object): | 
|  |  | 
|  | def __init__(self): | 
|  | self.process_maps = {}  # map from pid to a sorted list of MapEntry. | 
|  |  | 
|  | def add(self, pid, map_entry): | 
|  | old_list = self.process_maps.get(pid, []) | 
|  | new_list = [] | 
|  | map_entry_used = False | 
|  | for entry in old_list: | 
|  | if entry.end <= map_entry.start: | 
|  | new_list.append(entry) | 
|  | elif entry.start < map_entry.start: | 
|  | entry.end = map_entry.start | 
|  | new_list.append(entry) | 
|  | else: | 
|  | if not map_entry_used: | 
|  | new_list.append(map_entry) | 
|  | map_entry_used = True | 
|  | if entry.start >= map_entry.end: | 
|  | new_list.append(entry) | 
|  | elif entry.end > map_entry.end: | 
|  | entry.start = map_entry.end | 
|  | new_list.append(entry) | 
|  | if not map_entry_used: | 
|  | new_list.append(map_entry) | 
|  | self.process_maps[pid] = new_list | 
|  |  | 
|  | def fork_pid(self, pid, ppid): | 
|  | if pid == ppid: | 
|  | return | 
|  | entry_list = self.process_maps.get(ppid, []) | 
|  | self.process_maps[pid] = copy.deepcopy(entry_list) | 
|  |  | 
|  | def find(self, pid, addr): | 
|  | key = MapEntry(addr, addr, '') | 
|  | entry_list = self.process_maps.get(pid, []) | 
|  | pos = bisect.bisect_right(entry_list, key) | 
|  | if pos > 0 and entry_list[pos - 1].end > addr: | 
|  | return entry_list[pos - 1] | 
|  | return None | 
|  |  | 
|  | def show(self): | 
|  | for pid in sorted(self.process_maps): | 
|  | print('  pid %d' % pid) | 
|  | for entry in self.process_maps[pid]: | 
|  | print('    map [%x-%x] %s' % | 
|  | (entry.start, entry.end, entry.filename)) | 
|  |  | 
|  |  | 
|  | class UnwindingTimes(object): | 
|  |  | 
|  | def __init__(self): | 
|  | self.total_time = 0 | 
|  | self.count = 0 | 
|  | self.max_time = 0 | 
|  |  | 
|  | def add_time(self, used_time): | 
|  | self.total_time += used_time | 
|  | self.count += 1 | 
|  | self.max_time = max(self.max_time, used_time) | 
|  |  | 
|  |  | 
|  | class UnwindingMemConsumption(object): | 
|  |  | 
|  | def __init__(self): | 
|  | self.before_unwinding = None | 
|  | self.after_unwinding = None | 
|  |  | 
|  |  | 
|  | class CallChainNode(object): | 
|  |  | 
|  | """ Representing a node in a call chain.""" | 
|  |  | 
|  | def __init__(self, ip, sp, filename, vaddr_in_file, function_name, map_start_addr, | 
|  | map_end_addr): | 
|  | self.ip = ip | 
|  | self.sp = sp | 
|  | self.filename = filename | 
|  | self.vaddr_in_file = vaddr_in_file | 
|  | self.function_name = function_name | 
|  | self.map_start_addr = map_start_addr | 
|  | self.map_end_addr = map_end_addr | 
|  |  | 
|  |  | 
|  | class SampleResult(object): | 
|  |  | 
|  | """ Unwinding result per sample. """ | 
|  |  | 
|  | def __init__(self, pid, tid, unwinding_result, callchain, sample_record): | 
|  | self.pid = pid | 
|  | self.tid = tid | 
|  | self.unwinding_result = unwinding_result | 
|  | self.callchain = callchain | 
|  | self.sample_record = sample_record | 
|  |  | 
|  | def show(self): | 
|  | print('  pid: %d' % self.pid) | 
|  | print('  tid: %d' % self.tid) | 
|  | for key, value in self.unwinding_result.items(): | 
|  | print('  %s: %s' % (key, value)) | 
|  | for i, node in enumerate(self.callchain): | 
|  | print('  node %d: ip 0x%x, sp 0x%x, %s (%s[+%x]), map [%x-%x]' % ( | 
|  | i, node.ip, node.sp, node.function_name, node.filename, node.vaddr_in_file, | 
|  | node.map_start_addr, node.map_end_addr)) | 
|  | if self.sample_record: | 
|  | print('  original sample record:') | 
|  | for line in self.sample_record: | 
|  | print('    %s' % line) | 
|  |  | 
|  |  | 
|  | class FunctionResult(object): | 
|  |  | 
|  | """ Unwinding result per function. """ | 
|  |  | 
|  | def __init__(self): | 
|  | # Map from Unwinding result reason to [SampleResult]. | 
|  | self.sample_results = {} | 
|  |  | 
|  | def add_sample_result(self, sample_result): | 
|  | stop_reason = sample_result.unwinding_result['stop_reason'] | 
|  | result_list = self.sample_results.get(stop_reason) | 
|  | if not result_list: | 
|  | result_list = self.sample_results[stop_reason] = [] | 
|  | for result in result_list: | 
|  | if result.callchain[-1].vaddr_in_file == sample_result.callchain[-1].vaddr_in_file: | 
|  | # This sample_result duplicates with an existing one. | 
|  | return | 
|  | # We don't want to store too many sample results for a function. | 
|  | if len(result_list) < 10: | 
|  | result_list.append(sample_result) | 
|  |  | 
|  | def show(self): | 
|  | for stop_reason in sorted(self.sample_results): | 
|  | for sample_result in self.sample_results[stop_reason]: | 
|  | sample_result.show() | 
|  |  | 
|  |  | 
|  | class FileResult(object): | 
|  |  | 
|  | """ Unwinding result per shared library. """ | 
|  |  | 
|  | def __init__(self): | 
|  | self.function_results = {}  # Map from function_name to FunctionResult. | 
|  |  | 
|  | def add_sample_result(self, sample_result): | 
|  | function_name = sample_result.callchain[-1].function_name | 
|  | function_result = self.function_results.get(function_name) | 
|  | if not function_result: | 
|  | function_result = self.function_results[ | 
|  | function_name] = FunctionResult() | 
|  | function_result.add_sample_result(sample_result) | 
|  |  | 
|  | def show(self): | 
|  | for function_name in sorted(self.function_results): | 
|  | print('  function %s' % function_name) | 
|  | self.function_results[function_name].show() | 
|  | print('\n') | 
|  |  | 
|  |  | 
|  | class UnwindingResultErrorReport(object): | 
|  |  | 
|  | """ Report time used for unwinding and unwinding result errors. """ | 
|  |  | 
|  | def __init__(self, omit_callchains_fixed_by_joiner): | 
|  | self.omit_callchains_fixed_by_joiner = omit_callchains_fixed_by_joiner | 
|  | self.process_maps = ProcessMaps() | 
|  | self.unwinding_times = UnwindingTimes() | 
|  | self.mem_stat = UnwindingMemConsumption() | 
|  | self.file_results = {}  # map from filename to FileResult. | 
|  |  | 
|  | def add_sample_result(self, sample_result, joined_record): | 
|  | self.unwinding_times.add_time(int(sample_result.unwinding_result['used_time'])) | 
|  | if self.should_omit(sample_result, joined_record): | 
|  | return | 
|  | filename = sample_result.callchain[-1].filename | 
|  | file_result = self.file_results.get(filename) | 
|  | if not file_result: | 
|  | file_result = self.file_results[filename] = FileResult() | 
|  | file_result.add_sample_result(sample_result) | 
|  |  | 
|  | def add_mem_stat(self, name, mem_str): | 
|  | # mem_str is like VmPeak:202464 kB;VmSize:183456 kB;VmHWM:98256 kB;VmRSS:33680 kB. | 
|  | items = [] | 
|  | for p in mem_str.split(';'): | 
|  | key, value = p.split(':') | 
|  | items.append((key, value)) | 
|  | if name == 'debug_unwind_mem_before': | 
|  | self.mem_stat.before_unwinding = items | 
|  | else: | 
|  | self.mem_stat.after_unwinding = items | 
|  |  | 
|  | def should_omit(self, sample_result, joined_record): | 
|  | # 1. Can't unwind code generated in memory. | 
|  | for name in ['/dev/ashmem/dalvik-jit-code-cache', '[anon:dalvik-jit-code-cache]', '//anon']: | 
|  | if name in sample_result.callchain[-1].filename: | 
|  | return True | 
|  | # 2. Don't report complete callchains, which can reach __libc_init or __start_thread in | 
|  | # libc.so. | 
|  | def is_callchain_complete(callchain): | 
|  | for node in callchain: | 
|  | if (node.filename.endswith('libc.so') and | 
|  | node.function_name in ['__libc_init', '__start_thread']): | 
|  | return True | 
|  | return False | 
|  | if is_callchain_complete(sample_result.callchain): | 
|  | return True | 
|  | # 3. Omit callchains made complete by callchain joiner. | 
|  | if self.omit_callchains_fixed_by_joiner and is_callchain_complete(joined_record.callchain): | 
|  | return True | 
|  | return False | 
|  |  | 
|  | def show(self): | 
|  | print('Unwinding time info:') | 
|  | print('  total time: %f ms' % (self.unwinding_times.total_time / 1e6)) | 
|  | print('  unwinding count: %d' % self.unwinding_times.count) | 
|  | if self.unwinding_times.count > 0: | 
|  | print('  average time: %f us' % ( | 
|  | self.unwinding_times.total_time / 1e3 / self.unwinding_times.count)) | 
|  | print('  max time: %f us' % (self.unwinding_times.max_time / 1e3)) | 
|  | print('Unwinding mem info:') | 
|  | for items in zip(self.mem_stat.before_unwinding, self.mem_stat.after_unwinding): | 
|  | assert items[0][0] == items[1][0] | 
|  | print('  %s: %s -> %s' % (items[0][0], items[0][1], items[1][1])) | 
|  | print('Process maps:') | 
|  | self.process_maps.show() | 
|  | for filename in sorted(self.file_results): | 
|  | print('filename %s' % filename) | 
|  | self.file_results[filename].show() | 
|  | print('\n') | 
|  |  | 
|  |  | 
|  | class CallChainRecord(object): | 
|  | """ Store data of a callchain record. """ | 
|  |  | 
|  | def __init__(self): | 
|  | self.pid = None | 
|  | self.tid = None | 
|  | self.callchain = [] | 
|  |  | 
|  | def parse_sample_record(lines, i): | 
|  | """ Read the lines belong to a SampleRecord.""" | 
|  | if i == len(lines) or not lines[i].startswith('record sample:'): | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | start_line = i | 
|  | i += 1 | 
|  | while i < len(lines) and (not lines[i] or lines[i].startswith(' ')): | 
|  | i += 1 | 
|  | return i, lines[start_line:i] | 
|  |  | 
|  | def parse_callchain_record(lines, i, chain_type, process_maps): | 
|  | if i == len(lines) or not lines[i].startswith('record callchain:'): | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | i += 1 | 
|  | record = CallChainRecord() | 
|  | ips = [] | 
|  | sps = [] | 
|  | function_names = [] | 
|  | filenames = [] | 
|  | vaddr_in_files = [] | 
|  | map_start_addrs = [] | 
|  | map_end_addrs = [] | 
|  | in_callchain = False | 
|  | while i < len(lines) and not lines[i].startswith('record'): | 
|  | line = lines[i].strip() | 
|  | items = line.split() | 
|  | if not items: | 
|  | i += 1 | 
|  | continue | 
|  | if items[0] == 'pid' and len(items) == 2: | 
|  | record.pid = int(items[1]) | 
|  | elif items[0] == 'tid' and len(items) == 2: | 
|  | record.tid = int(items[1]) | 
|  | elif items[0] == 'chain_type' and len(items) == 2: | 
|  | if items[1] != chain_type: | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | elif items[0] == 'ip': | 
|  | m = re.search(r'ip\s+0x(\w+),\s+sp\s+0x(\w+)$', line) | 
|  | if m: | 
|  | ips.append(int(m.group(1), 16)) | 
|  | sps.append(int(m.group(2), 16)) | 
|  | elif items[0] == 'callchain:': | 
|  | in_callchain = True | 
|  | elif in_callchain: | 
|  | # "dalvik-jit-code-cache (deleted)[+346c] ([anon:dalvik-jit-code-cache] | 
|  | #  (deleted)[+346c])" | 
|  | if re.search(r'\)\[\+\w+\]\)$', line): | 
|  | break_pos = line.rfind('(', 0, line.rfind('(')) | 
|  | else: | 
|  | break_pos = line.rfind('(') | 
|  | if break_pos > 0: | 
|  | m = re.match(r'(.*)\[\+(\w+)\]\)', line[break_pos + 1:]) | 
|  | if m: | 
|  | function_names.append(line[:break_pos].strip()) | 
|  | filenames.append(m.group(1)) | 
|  | vaddr_in_files.append(int(m.group(2), 16)) | 
|  | i += 1 | 
|  |  | 
|  | for ip in ips: | 
|  | map_entry = process_maps.find(record.pid, ip) | 
|  | if map_entry: | 
|  | map_start_addrs.append(map_entry.start) | 
|  | map_end_addrs.append(map_entry.end) | 
|  | else: | 
|  | map_start_addrs.append(0) | 
|  | map_end_addrs.append(0) | 
|  | n = len(ips) | 
|  | if (None in [record.pid, record.tid] or n == 0 or len(sps) != n or | 
|  | len(function_names) != n or len(filenames) != n or len(vaddr_in_files) != n or | 
|  | len(map_start_addrs) != n or len(map_end_addrs) != n): | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | for j in range(n): | 
|  | record.callchain.append(CallChainNode(ips[j], sps[j], filenames[j], vaddr_in_files[j], | 
|  | function_names[j], map_start_addrs[j], | 
|  | map_end_addrs[j])) | 
|  | return i, record | 
|  |  | 
|  |  | 
|  | def build_unwinding_result_report(args): | 
|  | simpleperf_path = get_host_binary_path('simpleperf') | 
|  | proc = subprocess.Popen([simpleperf_path, 'dump', args.record_file[0]], | 
|  | stdout=subprocess.PIPE) | 
|  | (stdoutdata, _) = proc.communicate() | 
|  | stdoutdata = bytes_to_str(stdoutdata) | 
|  | if 'debug_unwind = true' not in stdoutdata: | 
|  | log_exit("Can't parse unwinding result. Because " + | 
|  | "%s was not generated by the debug-unwind cmd." % args.record_file[0]) | 
|  | unwinding_report = UnwindingResultErrorReport(args.omit_callchains_fixed_by_joiner) | 
|  | process_maps = unwinding_report.process_maps | 
|  | lines = stdoutdata.split('\n') | 
|  | i = 0 | 
|  | while i < len(lines): | 
|  | if lines[i].startswith('record mmap:') or lines[i].startswith('record mmap2:'): | 
|  | i += 1 | 
|  | pid = None | 
|  | start = None | 
|  | end = None | 
|  | filename = None | 
|  | while i < len(lines) and not lines[i].startswith('record'): | 
|  | if lines[i].startswith('  pid'): | 
|  | m = re.search(r'pid\s+(\d+).+addr\s+0x(\w+).+len\s+0x(\w+)', lines[i]) | 
|  | if m: | 
|  | pid = int(m.group(1)) | 
|  | start = int(m.group(2), 16) | 
|  | end = start + int(m.group(3), 16) | 
|  | elif 'filename' in lines[i]: | 
|  | pos = lines[i].find('filename') + len('filename') | 
|  | filename = lines[i][pos:].strip() | 
|  | i += 1 | 
|  | if None in [pid, start, end, filename]: | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | process_maps.add(pid, MapEntry(start, end, filename)) | 
|  | elif lines[i].startswith('record unwinding_result:'): | 
|  | i += 1 | 
|  | unwinding_result = collections.OrderedDict() | 
|  | while i < len(lines) and not lines[i].startswith('record'): | 
|  | strs = (lines[i].strip()).split() | 
|  | if len(strs) == 2: | 
|  | unwinding_result[strs[0]] = strs[1] | 
|  | i += 1 | 
|  | for key in ['time', 'used_time', 'stop_reason']: | 
|  | if key not in unwinding_result: | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  |  | 
|  | i, sample_record = parse_sample_record(lines, i) | 
|  | i, original_record = parse_callchain_record(lines, i, 'ORIGINAL_OFFLINE', process_maps) | 
|  | i, joined_record = parse_callchain_record(lines, i, 'JOINED_OFFLINE', process_maps) | 
|  | if args.omit_sample: | 
|  | sample_record = [] | 
|  | sample_result = SampleResult(original_record.pid, original_record.tid, | 
|  | unwinding_result, original_record.callchain, | 
|  | sample_record) | 
|  | unwinding_report.add_sample_result(sample_result, joined_record) | 
|  | elif lines[i].startswith('record fork:'): | 
|  | i += 1 | 
|  | pid = None | 
|  | ppid = None | 
|  | while i < len(lines) and not lines[i].startswith('record'): | 
|  | if lines[i].startswith('  pid'): | 
|  | m = re.search(r'pid\s+(\w+),\s+ppid\s+(\w+)', lines[i]) | 
|  | if m: | 
|  | pid = int(m.group(1)) | 
|  | ppid = int(m.group(2)) | 
|  | i += 1 | 
|  | if None in [pid, ppid]: | 
|  | log_fatal('unexpected dump output near line %d' % i) | 
|  | process_maps.fork_pid(pid, ppid) | 
|  | elif lines[i].startswith('    debug_unwind_mem'): | 
|  | items = lines[i].strip().split(' = ') | 
|  | if len(items) == 2: | 
|  | unwinding_report.add_mem_stat(items[0], items[1]) | 
|  | i += 1 | 
|  | else: | 
|  | i += 1 | 
|  | return unwinding_report | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description='Report dwarf unwinding results generated by the debug-unwind cmd.') | 
|  | parser.add_argument('-i', '--record_file', nargs=1, default=['perf.data.debug'], help=""" | 
|  | Set profiling data to report. Default is perf.data.debug.""") | 
|  | parser.add_argument('--omit-callchains-fixed-by-joiner', action='store_true', help=""" | 
|  | Don't show incomplete callchains fixed by callchain joiner.""") | 
|  | parser.add_argument('--omit-sample', action='store_true', help="""Don't show original sample | 
|  | records.""") | 
|  | args = parser.parse_args() | 
|  | report = build_unwinding_result_report(args) | 
|  | report.show() | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |