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