1#!/usr/bin/env python3 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 18"""debug_unwind_reporter.py: report failed dwarf unwinding cases generated by debug-unwind cmd. 19 20Below is an example using debug_unwind_reporter.py: 211. Record with "-g --keep-failed-unwinding-debug-info" option on device. 22 $ simpleperf record -g --keep-failed-unwinding-debug-info --app com.google.sample.tunnel \\ 23 --duration 10 24 The generated perf.data can be used for normal reporting. But it also contains stack data 25 and binaries for debugging failed unwinding cases. 26 272. Generate report with debug-unwind cmd. 28 $ simpleperf debug-unwind -i perf.data --generate-report -o report.txt 29 The report contains details for each failed unwinding case. It is usually too long to 30 parse manually. That's why we need debug_unwind_reporter.py. 31 323. Use debug_unwind_reporter.py to parse the report. 33 $ simpleperf debug-unwind -i report.txt --summary 34 $ simpleperf debug-unwind -i report.txt --include-error-code 1 35 ... 36""" 37 38import argparse 39from collections import Counter, defaultdict 40from simpleperf_utils import ArgParseFormatter 41from texttable import Texttable 42from typing import Dict, Iterator, List 43 44 45class CallChainNode: 46 def __init__(self): 47 self.dso = '' 48 self.symbol = '' 49 50 51class Sample: 52 """ A failed unwinding case """ 53 54 def __init__(self, raw_lines: List[str]): 55 self.raw_lines = raw_lines 56 self.sample_time = 0 57 self.error_code = 0 58 self.callchain: List[CallChainNode] = [] 59 self.parse() 60 61 def parse(self): 62 for line in self.raw_lines: 63 key, value = line.split(': ', 1) 64 if key == 'sample_time': 65 self.sample_time = int(value) 66 elif key == 'unwinding_error_code': 67 self.error_code = int(value) 68 elif key.startswith('dso'): 69 callchain_id = int(key.rsplit('_', 1)[1]) 70 self._get_callchain_node(callchain_id).dso = value 71 elif key.startswith('symbol'): 72 callchain_id = int(key.rsplit('_', 1)[1]) 73 self._get_callchain_node(callchain_id).symbol = value 74 75 def _get_callchain_node(self, callchain_id: int) -> CallChainNode: 76 callchain_id -= 1 77 if callchain_id == len(self.callchain): 78 self.callchain.append(CallChainNode()) 79 return self.callchain[callchain_id] 80 81 82class SampleFilter: 83 def match(self, sample: Sample) -> bool: 84 raise Exception('unimplemented') 85 86 87class CompleteCallChainFilter(SampleFilter): 88 def match(self, sample: Sample) -> bool: 89 for node in sample.callchain: 90 if node.dso.endswith('libc.so') and (node.symbol in ('__libc_init', '__start_thread')): 91 return True 92 return False 93 94 95class ErrorCodeFilter(SampleFilter): 96 def __init__(self, error_code: List[int]): 97 self.error_code = set(error_code) 98 99 def match(self, sample: Sample) -> bool: 100 return sample.error_code in self.error_code 101 102 103class EndDsoFilter(SampleFilter): 104 def __init__(self, end_dso: List[str]): 105 self.end_dso = set(end_dso) 106 107 def match(self, sample: Sample) -> bool: 108 return sample.callchain[-1].dso in self.end_dso 109 110 111class EndSymbolFilter(SampleFilter): 112 def __init__(self, end_symbol: List[str]): 113 self.end_symbol = set(end_symbol) 114 115 def match(self, sample: Sample) -> bool: 116 return sample.callchain[-1].symbol in self.end_symbol 117 118 119class SampleTimeFilter(SampleFilter): 120 def __init__(self, sample_time: List[int]): 121 self.sample_time = set(sample_time) 122 123 def match(self, sample: Sample) -> bool: 124 return sample.sample_time in self.sample_time 125 126 127class ReportInput: 128 def __init__(self): 129 self.exclude_filters: List[SampleFilter] = [] 130 self.include_filters: List[SampleFilter] = [] 131 132 def set_filters(self, args: argparse.Namespace): 133 if not args.show_callchain_fixed_by_joiner: 134 self.exclude_filters.append(CompleteCallChainFilter()) 135 if args.exclude_error_code: 136 self.exclude_filters.append(ErrorCodeFilter(args.exclude_error_code)) 137 if args.exclude_end_dso: 138 self.exclude_filters.append(EndDsoFilter(args.exclude_end_dso)) 139 if args.exclude_end_symbol: 140 self.exclude_filters.append(EndSymbolFilter(args.exclude_end_symbol)) 141 if args.exclude_sample_time: 142 self.exclude_filters.append(SampleTimeFilter(args.exclude_sample_time)) 143 144 if args.include_error_code: 145 self.include_filters.append(ErrorCodeFilter(args.include_error_code)) 146 if args.include_end_dso: 147 self.include_filters.append(EndDsoFilter(args.include_end_dso)) 148 if args.include_end_symbol: 149 self.include_filters.append(EndSymbolFilter(args.include_end_symbol)) 150 if args.include_sample_time: 151 self.include_filters.append(SampleTimeFilter(args.include_sample_time)) 152 153 def get_samples(self, input_file: str) -> Iterator[Sample]: 154 sample_lines: List[str] = [] 155 in_sample = False 156 with open(input_file, 'r') as fh: 157 for line in fh.readlines(): 158 line = line.rstrip() 159 if line.startswith('sample_time:'): 160 in_sample = True 161 elif not line: 162 if in_sample: 163 in_sample = False 164 sample = Sample(sample_lines) 165 sample_lines = [] 166 if self.filter_sample(sample): 167 yield sample 168 if in_sample: 169 sample_lines.append(line) 170 171 def filter_sample(self, sample: Sample) -> bool: 172 """ Return true if the input sample passes filters. """ 173 for exclude_filter in self.exclude_filters: 174 if exclude_filter.match(sample): 175 return False 176 for include_filter in self.include_filters: 177 if not include_filter.match(sample): 178 return False 179 return True 180 181 182class ReportOutput: 183 def report(self, sample: Sample): 184 pass 185 186 def end_report(self): 187 pass 188 189 190class ReportOutputDetails(ReportOutput): 191 def report(self, sample: Sample): 192 for line in sample.raw_lines: 193 print(line) 194 print() 195 196 197class ReportOutputSummary(ReportOutput): 198 def __init__(self): 199 self.error_code_counter = Counter() 200 self.symbol_counters: Dict[int, Counter] = defaultdict(Counter) 201 202 def report(self, sample: Sample): 203 symbol_key = (sample.callchain[-1].dso, sample.callchain[-1].symbol) 204 self.symbol_counters[sample.error_code][symbol_key] += 1 205 self.error_code_counter[sample.error_code] += 1 206 207 def end_report(self): 208 self.draw_error_code_table() 209 self.draw_symbol_table() 210 211 def draw_error_code_table(self): 212 table = Texttable() 213 table.set_cols_align(['l', 'c']) 214 table.add_row(['Count', 'Error Code']) 215 for error_code, count in self.error_code_counter.most_common(): 216 table.add_row([count, error_code]) 217 print(table.draw()) 218 219 def draw_symbol_table(self): 220 table = Texttable() 221 table.set_cols_align(['l', 'c', 'l', 'l']) 222 table.add_row(['Count', 'Error Code', 'Dso', 'Symbol']) 223 for error_code, _ in self.error_code_counter.most_common(): 224 symbol_counter = self.symbol_counters[error_code] 225 for symbol_key, count in symbol_counter.most_common(): 226 dso, symbol = symbol_key 227 table.add_row([count, error_code, dso, symbol]) 228 print(table.draw()) 229 230 231def get_args() -> argparse.Namespace: 232 parser = argparse.ArgumentParser(description=__doc__, formatter_class=ArgParseFormatter) 233 parser.add_argument('-i', '--input-file', required=True, 234 help='report file generated by debug-unwind cmd') 235 parser.add_argument( 236 '--show-callchain-fixed-by-joiner', action='store_true', 237 help="""By default, we don't show failed unwinding cases fixed by callchain joiner. 238 Use this option to show them.""") 239 parser.add_argument('--summary', action='store_true', 240 help='show summary instead of case details') 241 parser.add_argument('--exclude-error-code', metavar='error_code', type=int, nargs='+', 242 help='exclude cases with selected error code') 243 parser.add_argument('--exclude-end-dso', metavar='dso', nargs='+', 244 help='exclude cases ending at selected binary') 245 parser.add_argument('--exclude-end-symbol', metavar='symbol', nargs='+', 246 help='exclude cases ending at selected symbol') 247 parser.add_argument('--exclude-sample-time', metavar='time', type=int, 248 nargs='+', help='exclude cases with selected sample time') 249 parser.add_argument('--include-error-code', metavar='error_code', type=int, 250 nargs='+', help='include cases with selected error code') 251 parser.add_argument('--include-end-dso', metavar='dso', nargs='+', 252 help='include cases ending at selected binary') 253 parser.add_argument('--include-end-symbol', metavar='symbol', nargs='+', 254 help='include cases ending at selected symbol') 255 parser.add_argument('--include-sample-time', metavar='time', type=int, 256 nargs='+', help='include cases with selected sample time') 257 return parser.parse_args() 258 259 260def main(): 261 args = get_args() 262 report_input = ReportInput() 263 report_input.set_filters(args) 264 report_output = ReportOutputSummary() if args.summary else ReportOutputDetails() 265 for sample in report_input.get_samples(args.input_file): 266 report_output.report(sample) 267 report_output.end_report() 268 269 270if __name__ == '__main__': 271 main() 272