#!/usr/bin/env python3 # # 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 failed dwarf unwinding cases generated by debug-unwind cmd. Below is an example using debug_unwind_reporter.py: 1. Record with "-g --keep-failed-unwinding-debug-info" option on device. $ simpleperf record -g --keep-failed-unwinding-debug-info --app com.google.sample.tunnel \\ --duration 10 The generated perf.data can be used for normal reporting. But it also contains stack data and binaries for debugging failed unwinding cases. 2. Generate report with debug-unwind cmd. $ simpleperf debug-unwind -i perf.data --generate-report -o report.txt The report contains details for each failed unwinding case. It is usually too long to parse manually. That's why we need debug_unwind_reporter.py. 3. Use debug_unwind_reporter.py to parse the report. $ simpleperf debug-unwind -i report.txt --summary $ simpleperf debug-unwind -i report.txt --include-error-code 1 ... """ import argparse from collections import Counter, defaultdict from simpleperf_utils import BaseArgumentParser from texttable import Texttable from typing import Dict, Iterator, List class CallChainNode: def __init__(self): self.dso = '' self.symbol = '' class Sample: """ A failed unwinding case """ def __init__(self, raw_lines: List[str]): self.raw_lines = raw_lines self.sample_time = 0 self.error_code = 0 self.callchain: List[CallChainNode] = [] self.parse() def parse(self): for line in self.raw_lines: key, value = line.split(': ', 1) if key == 'sample_time': self.sample_time = int(value) elif key == 'unwinding_error_code': self.error_code = int(value) elif key.startswith('dso'): callchain_id = int(key.rsplit('_', 1)[1]) self._get_callchain_node(callchain_id).dso = value elif key.startswith('symbol'): callchain_id = int(key.rsplit('_', 1)[1]) self._get_callchain_node(callchain_id).symbol = value def _get_callchain_node(self, callchain_id: int) -> CallChainNode: callchain_id -= 1 if callchain_id == len(self.callchain): self.callchain.append(CallChainNode()) return self.callchain[callchain_id] class SampleFilter: def match(self, sample: Sample) -> bool: raise Exception('unimplemented') class CompleteCallChainFilter(SampleFilter): def match(self, sample: Sample) -> bool: for node in sample.callchain: if node.dso.endswith('libc.so') and (node.symbol in ('__libc_init', '__start_thread')): return True return False class ErrorCodeFilter(SampleFilter): def __init__(self, error_code: List[int]): self.error_code = set(error_code) def match(self, sample: Sample) -> bool: return sample.error_code in self.error_code class EndDsoFilter(SampleFilter): def __init__(self, end_dso: List[str]): self.end_dso = set(end_dso) def match(self, sample: Sample) -> bool: return sample.callchain[-1].dso in self.end_dso class EndSymbolFilter(SampleFilter): def __init__(self, end_symbol: List[str]): self.end_symbol = set(end_symbol) def match(self, sample: Sample) -> bool: return sample.callchain[-1].symbol in self.end_symbol class SampleTimeFilter(SampleFilter): def __init__(self, sample_time: List[int]): self.sample_time = set(sample_time) def match(self, sample: Sample) -> bool: return sample.sample_time in self.sample_time class ReportInput: def __init__(self): self.exclude_filters: List[SampleFilter] = [] self.include_filters: List[SampleFilter] = [] def set_filters(self, args: argparse.Namespace): if not args.show_callchain_fixed_by_joiner: self.exclude_filters.append(CompleteCallChainFilter()) if args.exclude_error_code: self.exclude_filters.append(ErrorCodeFilter(args.exclude_error_code)) if args.exclude_end_dso: self.exclude_filters.append(EndDsoFilter(args.exclude_end_dso)) if args.exclude_end_symbol: self.exclude_filters.append(EndSymbolFilter(args.exclude_end_symbol)) if args.exclude_sample_time: self.exclude_filters.append(SampleTimeFilter(args.exclude_sample_time)) if args.include_error_code: self.include_filters.append(ErrorCodeFilter(args.include_error_code)) if args.include_end_dso: self.include_filters.append(EndDsoFilter(args.include_end_dso)) if args.include_end_symbol: self.include_filters.append(EndSymbolFilter(args.include_end_symbol)) if args.include_sample_time: self.include_filters.append(SampleTimeFilter(args.include_sample_time)) def get_samples(self, input_file: str) -> Iterator[Sample]: sample_lines: List[str] = [] in_sample = False with open(input_file, 'r') as fh: for line in fh.readlines(): line = line.rstrip() if line.startswith('sample_time:'): in_sample = True elif not line: if in_sample: in_sample = False sample = Sample(sample_lines) sample_lines = [] if self.filter_sample(sample): yield sample if in_sample: sample_lines.append(line) def filter_sample(self, sample: Sample) -> bool: """ Return true if the input sample passes filters. """ for exclude_filter in self.exclude_filters: if exclude_filter.match(sample): return False for include_filter in self.include_filters: if not include_filter.match(sample): return False return True class ReportOutput: def report(self, sample: Sample): pass def end_report(self): pass class ReportOutputDetails(ReportOutput): def report(self, sample: Sample): for line in sample.raw_lines: print(line) print() class ReportOutputSummary(ReportOutput): def __init__(self): self.error_code_counter = Counter() self.symbol_counters: Dict[int, Counter] = defaultdict(Counter) def report(self, sample: Sample): symbol_key = (sample.callchain[-1].dso, sample.callchain[-1].symbol) self.symbol_counters[sample.error_code][symbol_key] += 1 self.error_code_counter[sample.error_code] += 1 def end_report(self): self.draw_error_code_table() self.draw_symbol_table() def draw_error_code_table(self): table = Texttable() table.set_cols_align(['l', 'c']) table.add_row(['Count', 'Error Code']) for error_code, count in self.error_code_counter.most_common(): table.add_row([count, error_code]) print(table.draw()) def draw_symbol_table(self): table = Texttable() table.set_cols_align(['l', 'c', 'l', 'l']) table.add_row(['Count', 'Error Code', 'Dso', 'Symbol']) for error_code, _ in self.error_code_counter.most_common(): symbol_counter = self.symbol_counters[error_code] for symbol_key, count in symbol_counter.most_common(): dso, symbol = symbol_key table.add_row([count, error_code, dso, symbol]) print(table.draw()) def get_args() -> argparse.Namespace: parser = BaseArgumentParser(description=__doc__) parser.add_argument('-i', '--input-file', required=True, help='report file generated by debug-unwind cmd') parser.add_argument( '--show-callchain-fixed-by-joiner', action='store_true', help="""By default, we don't show failed unwinding cases fixed by callchain joiner. Use this option to show them.""") parser.add_argument('--summary', action='store_true', help='show summary instead of case details') parser.add_argument('--exclude-error-code', metavar='error_code', type=int, nargs='+', help='exclude cases with selected error code') parser.add_argument('--exclude-end-dso', metavar='dso', nargs='+', help='exclude cases ending at selected binary') parser.add_argument('--exclude-end-symbol', metavar='symbol', nargs='+', help='exclude cases ending at selected symbol') parser.add_argument('--exclude-sample-time', metavar='time', type=int, nargs='+', help='exclude cases with selected sample time') parser.add_argument('--include-error-code', metavar='error_code', type=int, nargs='+', help='include cases with selected error code') parser.add_argument('--include-end-dso', metavar='dso', nargs='+', help='include cases ending at selected binary') parser.add_argument('--include-end-symbol', metavar='symbol', nargs='+', help='include cases ending at selected symbol') parser.add_argument('--include-sample-time', metavar='time', type=int, nargs='+', help='include cases with selected sample time') return parser.parse_args() def main(): args = get_args() report_input = ReportInput() report_input.set_filters(args) report_output = ReportOutputSummary() if args.summary else ReportOutputDetails() for sample in report_input.get_samples(args.input_file): report_output.report(sample) report_output.end_report() if __name__ == '__main__': main()