Use version_utils for _MI_VERSION
[deliverable/lttng-analyses.git] / lttnganalyses / cli / command.py
CommitLineData
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 25import argparse
a0acc08c
PP
26import json
27import re
0b250a71
AB
28import sys
29import subprocess
30from babeltrace import TraceCollection
a0acc08c 31from . import mi
0b250a71
AB
32from .. import _version
33from . import progressbar
34from .. import __version__
3101128e 35from ..common import version_utils
0b250a71
AB
36from ..core import analysis
37from ..linuxautomaton import common
38from ..linuxautomaton import automaton
323b3fd6
PP
39
40
41class 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 498Command._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)
This page took 0.048266 seconds and 5 git commands to generate.