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