| #!/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() |