| #!/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 ArgParseFormatter | 
 | 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 = argparse.ArgumentParser(description=__doc__, formatter_class=ArgParseFormatter) | 
 |     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() |