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