Commit | Line | Data |
---|---|---|
4ed24f86 JD |
1 | # The MIT License (MIT) |
2 | # | |
a3fa57c0 | 3 | # Copyright (C) 2015 - Julien Desfossez <jdesfossez@efficios.com> |
cee855a2 | 4 | # 2015 - Philippe Proulx <pproulx@efficios.com> |
0b250a71 | 5 | # 2015 - Antoine Busque <abusque@efficios.com> |
4ed24f86 JD |
6 | # |
7 | # Permission is hereby granted, free of charge, to any person obtaining a copy | |
8 | # of this software and associated documentation files (the "Software"), to deal | |
9 | # in the Software without restriction, including without limitation the rights | |
10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
11 | # copies of the Software, and to permit persons to whom the Software is | |
12 | # furnished to do so, subject to the following conditions: | |
13 | # | |
14 | # The above copyright notice and this permission notice shall be included in | |
15 | # all copies or substantial portions of the Software. | |
16 | # | |
17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
23 | # SOFTWARE. | |
24 | ||
323b3fd6 | 25 | import argparse |
a0acc08c PP |
26 | import json |
27 | import re | |
0b250a71 AB |
28 | import sys |
29 | import subprocess | |
30 | from babeltrace import TraceCollection | |
a0acc08c | 31 | from . import mi |
0b250a71 AB |
32 | from .. import _version |
33 | from . import progressbar | |
34 | from .. import __version__ | |
3101128e | 35 | from ..common import version_utils |
0b250a71 AB |
36 | from ..core import analysis |
37 | from ..linuxautomaton import common | |
38 | from ..linuxautomaton import automaton | |
323b3fd6 PP |
39 | |
40 | ||
41 | class Command: | |
a0acc08c PP |
42 | _MI_BASE_TAGS = ['linux-kernel', 'lttng-analyses'] |
43 | _MI_AUTHORS = [ | |
44 | 'Julien Desfossez', | |
45 | 'Antoine Busque', | |
46 | 'Philippe Proulx', | |
47 | ] | |
48 | _MI_URL = 'https://github.com/lttng/lttng-analyses' | |
49 | ||
50 | def __init__(self, mi_mode=False): | |
b6d9132b AB |
51 | self._analysis = None |
52 | self._analysis_conf = None | |
53 | self._args = None | |
54 | self._handles = None | |
55 | self._traces = None | |
a0acc08c PP |
56 | self._ticks = 0 |
57 | self._mi_mode = mi_mode | |
323b3fd6 | 58 | self._create_automaton() |
a0acc08c PP |
59 | self._mi_setup() |
60 | ||
61 | @property | |
62 | def mi_mode(self): | |
63 | return self._mi_mode | |
323b3fd6 | 64 | |
b6d9132b | 65 | def run(self): |
74d112b5 AB |
66 | try: |
67 | self._parse_args() | |
68 | self._open_trace() | |
69 | self._create_analysis() | |
70 | self._run_analysis() | |
71 | self._close_trace() | |
72 | except KeyboardInterrupt: | |
73 | sys.exit(0) | |
b6d9132b | 74 | |
323b3fd6 | 75 | def _error(self, msg, exit_code=1): |
d6c76c60 PP |
76 | try: |
77 | import termcolor | |
78 | ||
79 | msg = termcolor.colored(msg, 'red', attrs=['bold']) | |
05684c5e | 80 | except ImportError: |
d6c76c60 PP |
81 | pass |
82 | ||
323b3fd6 PP |
83 | print(msg, file=sys.stderr) |
84 | sys.exit(exit_code) | |
85 | ||
86 | def _gen_error(self, msg, exit_code=1): | |
87 | self._error('Error: {}'.format(msg), exit_code) | |
88 | ||
89 | def _cmdline_error(self, msg, exit_code=1): | |
90 | self._error('Command line error: {}'.format(msg), exit_code) | |
91 | ||
a0acc08c PP |
92 | def _print(self, msg): |
93 | if not self._mi_mode: | |
94 | print(msg) | |
95 | ||
96 | def _mi_create_result_table(self, table_class_name, begin, end, | |
97 | subtitle=None): | |
98 | return mi.ResultTable(self._mi_table_classes[table_class_name], | |
99 | begin, end, subtitle) | |
100 | ||
101 | def _mi_setup(self): | |
102 | self._mi_table_classes = {} | |
103 | ||
104 | for tc_tuple in self._MI_TABLE_CLASSES: | |
105 | table_class = mi.TableClass(tc_tuple[0], tc_tuple[1], tc_tuple[2]) | |
106 | self._mi_table_classes[table_class.name] = table_class | |
107 | ||
108 | self._mi_clear_result_tables() | |
109 | ||
110 | def _mi_print_metadata(self): | |
111 | tags = self._MI_BASE_TAGS + self._MI_TAGS | |
112 | infos = mi.get_metadata(version=self._MI_VERSION, title=self._MI_TITLE, | |
113 | description=self._MI_DESCRIPTION, | |
114 | authors=self._MI_AUTHORS, url=self._MI_URL, | |
115 | tags=tags, | |
116 | table_classes=self._mi_table_classes.values()) | |
117 | print(json.dumps(infos)) | |
118 | ||
119 | def _mi_append_result_table(self, result_table): | |
120 | if not result_table or not result_table.rows: | |
121 | return | |
122 | ||
123 | tc_name = result_table.table_class.name | |
124 | self._mi_get_result_tables(tc_name).append(result_table) | |
125 | ||
126 | def _mi_append_result_tables(self, result_tables): | |
127 | if not result_tables: | |
128 | return | |
129 | ||
130 | for result_table in result_tables: | |
131 | self._mi_append_result_table(result_table) | |
132 | ||
133 | def _mi_clear_result_tables(self): | |
134 | self._result_tables = {} | |
135 | ||
136 | def _mi_get_result_tables(self, table_class_name): | |
137 | if table_class_name not in self._result_tables: | |
138 | self._result_tables[table_class_name] = [] | |
139 | ||
140 | return self._result_tables[table_class_name] | |
141 | ||
142 | def _mi_print(self): | |
143 | results = [] | |
144 | ||
145 | for result_tables in self._result_tables.values(): | |
146 | for result_table in result_tables: | |
147 | results.append(result_table.to_native_object()) | |
148 | ||
149 | obj = { | |
150 | 'results': results, | |
151 | } | |
152 | ||
153 | print(json.dumps(obj)) | |
154 | ||
155 | def _create_summary_result_tables(self): | |
156 | pass | |
157 | ||
bd3cd7c5 JD |
158 | def _open_trace(self): |
159 | traces = TraceCollection() | |
b6d9132b | 160 | handles = traces.add_traces_recursive(self._args.path, 'ctf') |
ced36aab | 161 | if handles == {}: |
b6d9132b | 162 | self._gen_error('Failed to open ' + self._args.path, -1) |
ced36aab | 163 | self._handles = handles |
bd3cd7c5 | 164 | self._traces = traces |
652bc6b7 | 165 | self._process_date_args() |
b6d9132b | 166 | if not self._args.skip_validation: |
d3014022 | 167 | self._check_lost_events() |
bd3cd7c5 JD |
168 | |
169 | def _close_trace(self): | |
ced36aab AB |
170 | for handle in self._handles.values(): |
171 | self._traces.remove_trace(handle) | |
bd3cd7c5 | 172 | |
d3014022 | 173 | def _check_lost_events(self): |
a0acc08c | 174 | self._print('Checking the trace for lost events...') |
d3014022 | 175 | try: |
e0bc16fe | 176 | subprocess.check_output('babeltrace "%s"' % self._args.path, |
d3014022 JD |
177 | shell=True) |
178 | except subprocess.CalledProcessError: | |
b9f05f8d AB |
179 | self._gen_error('Cannot run babeltrace on the trace, cannot verify' |
180 | ' if events were lost during the trace recording') | |
a0acc08c PP |
181 | |
182 | def _pre_analysis(self): | |
183 | pass | |
184 | ||
185 | def _post_analysis(self): | |
186 | if not self._mi_mode: | |
187 | return | |
188 | ||
189 | if self._ticks > 1: | |
190 | self._create_summary_result_tables() | |
191 | ||
192 | self._mi_print() | |
d3014022 | 193 | |
b6d9132b | 194 | def _run_analysis(self): |
a0acc08c | 195 | self._pre_analysis() |
bd3cd7c5 | 196 | progressbar.progressbar_setup(self) |
b6d9132b | 197 | |
bd3cd7c5 JD |
198 | for event in self._traces.events: |
199 | progressbar.progressbar_update(self) | |
bd3cd7c5 | 200 | self._analysis.process_event(event) |
b6d9132b AB |
201 | if self._analysis.ended: |
202 | break | |
47ba125c | 203 | self._automaton.process_event(event) |
bd3cd7c5 | 204 | |
b6d9132b AB |
205 | progressbar.progressbar_finish(self) |
206 | self._analysis.end() | |
a0acc08c | 207 | self._post_analysis() |
bd3cd7c5 | 208 | |
3664e4b0 AB |
209 | def _print_date(self, begin_ns, end_ns): |
210 | date = 'Timerange: [%s, %s]' % ( | |
b6d9132b | 211 | common.ns_to_hour_nsec(begin_ns, gmt=self._args.gmt, |
3664e4b0 | 212 | multi_day=True), |
b6d9132b | 213 | common.ns_to_hour_nsec(end_ns, gmt=self._args.gmt, |
3664e4b0 | 214 | multi_day=True)) |
a0acc08c | 215 | self._print(date) |
3664e4b0 | 216 | |
dbbdd963 PP |
217 | def _get_uniform_freq_values(self, durations): |
218 | if self._args.uniform_step is not None: | |
219 | return (self._args.uniform_min, self._args.uniform_max, | |
220 | self._args.uniform_step) | |
221 | ||
222 | if self._args.min is not None: | |
223 | self._args.uniform_min = self._args.min | |
224 | else: | |
225 | self._args.uniform_min = min(durations) | |
226 | if self._args.max is not None: | |
227 | self._args.uniform_max = self._args.max | |
228 | else: | |
229 | self._args.uniform_max = max(durations) | |
230 | ||
231 | # ns to µs | |
232 | self._args.uniform_min /= 1000 | |
233 | self._args.uniform_max /= 1000 | |
234 | self._args.uniform_step = ( | |
235 | (self._args.uniform_max - self._args.uniform_min) / | |
236 | self._args.freq_resolution | |
237 | ) | |
238 | ||
239 | return self._args.uniform_min, self._args.uniform_max, \ | |
650e7f57 | 240 | self._args.uniform_step |
dbbdd963 | 241 | |
bd3cd7c5 | 242 | def _validate_transform_common_args(self, args): |
83ad157b AB |
243 | refresh_period_ns = None |
244 | if args.refresh is not None: | |
245 | try: | |
246 | refresh_period_ns = common.duration_str_to_ns(args.refresh) | |
247 | except ValueError as e: | |
248 | self._cmdline_error(str(e)) | |
249 | ||
b6d9132b | 250 | self._analysis_conf = analysis.AnalysisConfig() |
83ad157b | 251 | self._analysis_conf.refresh_period = refresh_period_ns |
43a3c04c AB |
252 | self._analysis_conf.period_begin_ev_name = args.period_begin |
253 | self._analysis_conf.period_end_ev_name = args.period_end | |
05684c5e AB |
254 | self._analysis_conf.period_begin_key_fields = \ |
255 | args.period_begin_key.split(',') | |
256 | ||
257 | if args.period_end_key: | |
258 | self._analysis_conf.period_end_key_fields = \ | |
259 | args.period_end_key.split(',') | |
260 | else: | |
261 | self._analysis_conf.period_end_key_fields = \ | |
262 | self._analysis_conf.period_begin_key_fields | |
263 | ||
264 | if args.period_key_value: | |
265 | self._analysis_conf.period_key_value = \ | |
266 | tuple(args.period_key_value.split(',')) | |
267 | ||
a621ba35 AB |
268 | if args.cpu: |
269 | self._analysis_conf.cpu_list = args.cpu.split(',') | |
270 | self._analysis_conf.cpu_list = [int(cpu) for cpu in | |
271 | self._analysis_conf.cpu_list] | |
b6d9132b AB |
272 | |
273 | # convert min/max args from µs to ns, if needed | |
274 | if hasattr(args, 'min') and args.min is not None: | |
275 | args.min *= 1000 | |
276 | self._analysis_conf.min_duration = args.min | |
277 | if hasattr(args, 'max') and args.max is not None: | |
278 | args.max *= 1000 | |
279 | self._analysis_conf.max_duration = args.max | |
280 | ||
281 | if hasattr(args, 'procname'): | |
47ba125c | 282 | if args.procname: |
43b66dd6 | 283 | self._analysis_conf.proc_list = args.procname.split(',') |
28ad5ec8 | 284 | |
43b66dd6 AB |
285 | if hasattr(args, 'tid'): |
286 | if args.tid: | |
287 | self._analysis_conf.tid_list = args.tid.split(',') | |
288 | self._analysis_conf.tid_list = [int(tid) for tid in | |
289 | self._analysis_conf.tid_list] | |
f89605f0 | 290 | |
1a68e04c AB |
291 | if hasattr(args, 'freq'): |
292 | args.uniform_min = None | |
293 | args.uniform_max = None | |
294 | args.uniform_step = None | |
295 | ||
dbbdd963 PP |
296 | if args.freq_series: |
297 | # implies uniform buckets | |
298 | args.freq_uniform = True | |
299 | ||
a0acc08c PP |
300 | if self._mi_mode: |
301 | # force no progress in MI mode | |
302 | args.no_progress = True | |
303 | ||
304 | # print MI metadata if required | |
305 | if args.metadata: | |
306 | self._mi_print_metadata() | |
307 | sys.exit(0) | |
308 | ||
309 | # validate path argument (required at this point) | |
310 | if not args.path: | |
311 | self._cmdline_error('Please specify a trace path') | |
312 | ||
313 | if type(args.path) is list: | |
314 | args.path = args.path[0] | |
315 | ||
b6d9132b AB |
316 | def _validate_transform_args(self, args): |
317 | pass | |
f89605f0 | 318 | |
323b3fd6 PP |
319 | def _parse_args(self): |
320 | ap = argparse.ArgumentParser(description=self._DESC) | |
321 | ||
322 | # common arguments | |
83ad157b AB |
323 | ap.add_argument('-r', '--refresh', type=str, |
324 | help='Refresh period, with optional units suffix ' | |
325 | '(default units: s)') | |
a0acc08c PP |
326 | ap.add_argument('--gmt', action='store_true', |
327 | help='Manipulate timestamps based on GMT instead ' | |
328 | 'of local time') | |
73b71522 | 329 | ap.add_argument('--skip-validation', action='store_true', |
d3014022 | 330 | help='Skip the trace validation') |
bd3cd7c5 JD |
331 | ap.add_argument('--begin', type=str, help='start time: ' |
332 | 'hh:mm:ss[.nnnnnnnnn]') | |
333 | ap.add_argument('--end', type=str, help='end time: ' | |
334 | 'hh:mm:ss[.nnnnnnnnn]') | |
43a3c04c AB |
335 | ap.add_argument('--period-begin', type=str, |
336 | help='Analysis period start marker event name') | |
337 | ap.add_argument('--period-end', type=str, | |
338 | help='Analysis period end marker event name ' | |
339 | '(requires --period-begin)') | |
05684c5e | 340 | ap.add_argument('--period-begin-key', type=str, default='cpu_id', |
b9f05f8d AB |
341 | help='Optional, list of event field names used to ' |
342 | 'match period markers (default: cpu_id)') | |
05684c5e AB |
343 | ap.add_argument('--period-end-key', type=str, |
344 | help='Optional, list of event field names used to ' | |
345 | 'match period marker. If none specified, use the same ' | |
346 | ' --period-begin-key') | |
347 | ap.add_argument('--period-key-value', type=str, | |
348 | help='Optional, define a fixed key value to which a' | |
349 | ' period must correspond to be considered.') | |
a621ba35 AB |
350 | ap.add_argument('--cpu', type=str, |
351 | help='Filter the results only for this list of ' | |
352 | 'CPU IDs') | |
a0acc08c PP |
353 | ap.add_argument('--timerange', type=str, help='time range: ' |
354 | '[begin,end]') | |
323b3fd6 | 355 | ap.add_argument('-V', '--version', action='version', |
d97f5cb2 | 356 | version='LTTng Analyses v' + __version__) |
323b3fd6 | 357 | |
a0acc08c PP |
358 | # MI mode-dependent arguments |
359 | if self._mi_mode: | |
360 | ap.add_argument('--metadata', action='store_true', | |
b9f05f8d AB |
361 | help='Show analysis\'s metadata') |
362 | ap.add_argument('path', metavar='<path/to/trace>', | |
363 | help='trace path', nargs='*') | |
a0acc08c PP |
364 | else: |
365 | ap.add_argument('--no-progress', action='store_true', | |
366 | help='Don\'t display the progress bar') | |
b9f05f8d AB |
367 | ap.add_argument('path', metavar='<path/to/trace>', |
368 | help='trace path') | |
a0acc08c | 369 | |
b6d9132b AB |
370 | # Used to add command-specific args |
371 | self._add_arguments(ap) | |
323b3fd6 | 372 | |
b6d9132b | 373 | args = ap.parse_args() |
bd3cd7c5 | 374 | self._validate_transform_common_args(args) |
b6d9132b | 375 | self._validate_transform_args(args) |
323b3fd6 PP |
376 | self._args = args |
377 | ||
b6d9132b AB |
378 | @staticmethod |
379 | def _add_proc_filter_args(ap): | |
380 | ap.add_argument('--procname', type=str, | |
381 | help='Filter the results only for this list of ' | |
382 | 'process names') | |
43b66dd6 AB |
383 | ap.add_argument('--tid', type=str, |
384 | help='Filter the results only for this list of TIDs') | |
b6d9132b AB |
385 | |
386 | @staticmethod | |
387 | def _add_min_max_args(ap): | |
388 | ap.add_argument('--min', type=float, | |
389 | help='Filter out durations shorter than min usec') | |
390 | ap.add_argument('--max', type=float, | |
391 | help='Filter out durations longer than max usec') | |
392 | ||
393 | @staticmethod | |
394 | def _add_freq_args(ap, help=None): | |
395 | if not help: | |
396 | help = 'Output the frequency distribution' | |
397 | ||
398 | ap.add_argument('--freq', action='store_true', help=help) | |
399 | ap.add_argument('--freq-resolution', type=int, default=20, | |
400 | help='Frequency distribution resolution ' | |
401 | '(default 20)') | |
1a68e04c AB |
402 | ap.add_argument('--freq-uniform', action='store_true', |
403 | help='Use a uniform resolution across distributions') | |
86ea0394 | 404 | ap.add_argument('--freq-series', action='store_true', |
650e7f57 AB |
405 | help='Consolidate frequency distribution histogram ' |
406 | 'as a single one') | |
b6d9132b AB |
407 | |
408 | @staticmethod | |
409 | def _add_log_args(ap, help=None): | |
410 | if not help: | |
411 | help = 'Output the events in chronological order' | |
412 | ||
413 | ap.add_argument('--log', action='store_true', help=help) | |
414 | ||
b9f05f8d AB |
415 | @staticmethod |
416 | def _add_top_args(ap, help=None): | |
417 | if not help: | |
418 | help = 'Output the top results' | |
419 | ||
420 | ap.add_argument('--limit', type=int, default=10, | |
421 | help='Limit to top X (default = 10)') | |
422 | ap.add_argument('--top', action='store_true', help=help) | |
423 | ||
b6d9132b AB |
424 | @staticmethod |
425 | def _add_stats_args(ap, help=None): | |
426 | if not help: | |
427 | help = 'Output statistics' | |
428 | ||
429 | ap.add_argument('--stats', action='store_true', help=help) | |
430 | ||
431 | def _add_arguments(self, ap): | |
432 | pass | |
433 | ||
652bc6b7 | 434 | def _process_date_args(self): |
b6d9132b AB |
435 | def date_to_epoch_nsec(date): |
436 | ts = common.date_to_epoch_nsec(self._handles, date, self._args.gmt) | |
437 | if ts is None: | |
438 | self._cmdline_error('Invalid date format: "{}"'.format(date)) | |
439 | ||
440 | return ts | |
441 | ||
442 | self._args.multi_day = common.is_multi_day_trace_collection( | |
652bc6b7 | 443 | self._handles) |
602ac199 PP |
444 | begin_ts = None |
445 | end_ts = None | |
446 | ||
447 | if self._args.timerange: | |
448 | begin_ts, end_ts = common.extract_timerange(self._handles, | |
449 | self._args.timerange, | |
450 | self._args.gmt) | |
451 | if None in [begin_ts, end_ts]: | |
b9f05f8d AB |
452 | self._cmdline_error( |
453 | 'Invalid time format: "{}"'.format(self._args.timerange)) | |
652bc6b7 | 454 | else: |
b6d9132b | 455 | if self._args.begin: |
602ac199 | 456 | begin_ts = date_to_epoch_nsec(self._args.begin) |
b6d9132b | 457 | if self._args.end: |
602ac199 | 458 | end_ts = date_to_epoch_nsec(self._args.end) |
652bc6b7 | 459 | |
93c7af7d AB |
460 | # We have to check if timestamp_begin is None, which |
461 | # it always is in older versions of babeltrace. In | |
462 | # that case, the test is simply skipped and an invalid | |
463 | # --end value will cause an empty analysis | |
464 | if self._traces.timestamp_begin is not None and \ | |
602ac199 | 465 | end_ts < self._traces.timestamp_begin: |
b6d9132b AB |
466 | self._cmdline_error( |
467 | '--end timestamp before beginning of trace') | |
468 | ||
602ac199 PP |
469 | self._analysis_conf.begin_ts = begin_ts |
470 | self._analysis_conf.end_ts = end_ts | |
b6d9132b AB |
471 | |
472 | def _create_analysis(self): | |
473 | notification_cbs = { | |
a0acc08c | 474 | analysis.Analysis.TICK_CB: self._analysis_tick_cb |
b6d9132b AB |
475 | } |
476 | ||
477 | self._analysis = self._ANALYSIS_CLASS(self.state, self._analysis_conf) | |
478 | self._analysis.register_notification_cbs(notification_cbs) | |
93c7af7d | 479 | |
323b3fd6 | 480 | def _create_automaton(self): |
56936af2 | 481 | self._automaton = automaton.Automaton() |
6e01ed18 | 482 | self.state = self._automaton.state |
bfb81992 | 483 | |
a0acc08c | 484 | def _analysis_tick_cb(self, **kwargs): |
b6d9132b AB |
485 | begin_ns = kwargs['begin_ns'] |
486 | end_ns = kwargs['end_ns'] | |
487 | ||
a0acc08c PP |
488 | self._analysis_tick(begin_ns, end_ns) |
489 | self._ticks += 1 | |
b6d9132b | 490 | |
a0acc08c | 491 | def _analysis_tick(self, begin_ns, end_ns): |
b6d9132b AB |
492 | raise NotImplementedError() |
493 | ||
a0acc08c PP |
494 | |
495 | # create MI version | |
496 | _cmd_version = _version.get_versions()['version'] | |
497 | _version_match = re.match(r'(\d+)\.(\d+)\.(\d+)(.*)', _cmd_version) | |
3101128e | 498 | Command._MI_VERSION = version_utils.Version( |
a0acc08c PP |
499 | int(_version_match.group(1)), |
500 | int(_version_match.group(2)), | |
501 | int(_version_match.group(3)), | |
502 | _version_match.group(4), | |
3101128e | 503 | ) |