• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2024 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import argparse
18import os
19from .command import ProfilerCommand, ConfigCommand, OpenCommand
20from .device import AdbDevice
21from .validation_error import ValidationError
22from .config_builder import PREDEFINED_PERFETTO_CONFIGS
23from .utils import path_exists, set_default_subparser
24from .validate_simpleperf import verify_simpleperf_args
25from .vm import add_vm_parser, create_vm_command
26
27# Add default parser capability to argparse
28argparse.ArgumentParser.set_default_subparser = set_default_subparser
29
30DEFAULT_DUR_MS = 10000
31MIN_DURATION_MS = 3000
32DEFAULT_OUT_DIR = "."
33
34
35def create_parser():
36  parser = argparse.ArgumentParser(prog='torq command',
37                                   description=('Torq CLI tool for performance'
38                                                ' tests.'))
39  # Global options
40  # NOTE: All global options must have the 'nargs' option set to an int.
41  parser.add_argument('--serial', nargs=1,
42                      help=(('Specifies serial of the device that will be'
43                             ' used.')))
44
45  # Subparsers
46  subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
47
48  # Profiler options
49  profiler_parser = subparsers.add_parser('profiler', help=('Profiler subcommand'
50                                                            ' used to trace and'
51                                                            ' profile Android'))
52  profiler_parser.add_argument('-e', '--event',
53                      choices=['boot', 'user-switch', 'app-startup', 'custom'],
54                      default='custom', help='The event to trace/profile.')
55  profiler_parser.add_argument('-p', '--profiler', choices=['perfetto', 'simpleperf'],
56                      default='perfetto', help='The performance data source.')
57  profiler_parser.add_argument('-o', '--out-dir', default=DEFAULT_OUT_DIR,
58                      help='The path to the output directory.')
59  profiler_parser.add_argument('-d', '--dur-ms', type=int, default=DEFAULT_DUR_MS,
60                      help=('The duration (ms) of the event. Determines when'
61                            ' to stop collecting performance data.'))
62  profiler_parser.add_argument('-a', '--app',
63                      help='The package name of the app we want to start.')
64  profiler_parser.add_argument('-r', '--runs', type=int, default=1,
65                      help=('The number of times to run the event and'
66                            ' capture the perf data.'))
67  profiler_parser.add_argument('-s', '--simpleperf-event', action='append',
68                      help=('Simpleperf supported events to be collected.'
69                            ' e.g. cpu-cycles, instructions'))
70  profiler_parser.add_argument('--perfetto-config', default='default',
71                      help=('Predefined perfetto configs can be used:'
72                            ' %s. A filepath with a custom config could'
73                            ' also be provided.'
74                            % (", ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
75  profiler_parser.add_argument('--between-dur-ms', type=int, default=DEFAULT_DUR_MS,
76                      help='Time (ms) to wait before executing the next event.')
77  profiler_parser.add_argument('--ui', action=argparse.BooleanOptionalAction,
78                      help=('Specifies opening of UI visualization tool'
79                            ' after profiling is complete.'))
80  profiler_parser.add_argument('--excluded-ftrace-events', action='append',
81                      help=('Excludes specified ftrace event from the perfetto'
82                            ' config events.'))
83  profiler_parser.add_argument('--included-ftrace-events', action='append',
84                      help=('Includes specified ftrace event in the perfetto'
85                            ' config events.'))
86  profiler_parser.add_argument('--from-user', type=int,
87                      help='The user id from which to start the user switch')
88  profiler_parser.add_argument('--to-user', type=int,
89                      help='The user id of user that system is switching to.')
90  profiler_parser.add_argument('--symbols',
91                      help='Specifies path to symbols library.')
92
93  # Config options
94  config_parser = subparsers.add_parser('config',
95                                        help=('The config subcommand used'
96                                              ' to list and show the'
97                                              ' predefined perfetto configs.'))
98  config_subparsers = config_parser.add_subparsers(dest='config_subcommand',
99                                                   help=('torq config'
100                                                         ' subcommands'))
101  config_subparsers.add_parser('list',
102                               help=('Command to list the predefined'
103                                     ' perfetto configs'))
104  config_show_parser = config_subparsers.add_parser('show',
105                                                    help=('Command to print'
106                                                          ' the '
107                                                          ' perfetto config'
108                                                          ' in the terminal.'))
109  config_show_parser.add_argument('config_name',
110                                  choices=['lightweight', 'default', 'memory'],
111                                  help=('Name of the predefined perfetto'
112                                        ' config to print.'))
113  config_show_parser.add_argument('-d', '--dur-ms', type=int, default=DEFAULT_DUR_MS,
114                      help=('The duration (ms) of the event. Determines when'
115                            ' to stop collecting performance data.'))
116  config_show_parser.add_argument('--excluded-ftrace-events', action='append',
117                      help=('Excludes specified ftrace event from the perfetto'
118                            ' config events.'))
119  config_show_parser.add_argument('--included-ftrace-events', action='append',
120                      help=('Includes specified ftrace event in the perfetto'
121                            ' config events.'))
122
123  config_pull_parser = config_subparsers.add_parser('pull',
124                                                    help=('Command to copy'
125                                                          ' a predefined config'
126                                                          ' to the specified'
127                                                          ' file path.'))
128  config_pull_parser.add_argument('config_name',
129                                  choices=['lightweight', 'default', 'memory'],
130                                  help='Name of the predefined config to copy')
131  config_pull_parser.add_argument('file_path', nargs='?',
132                                  help=('File path to copy the predefined'
133                                        ' config to'))
134  config_pull_parser.add_argument('-d', '--dur-ms', type=int, default=DEFAULT_DUR_MS,
135                      help=('The duration (ms) of the event. Determines when'
136                            ' to stop collecting performance data.'))
137  config_pull_parser.add_argument('--excluded-ftrace-events', action='append',
138                      help=('Excludes specified ftrace event from the perfetto'
139                            ' config events.'))
140  config_pull_parser.add_argument('--included-ftrace-events', action='append',
141                      help=('Includes specified ftrace event in the perfetto'
142                            ' config events.'))
143
144  # Open options
145  open_parser = subparsers.add_parser('open',
146                                      help=('The open subcommand is used '
147                                            'to open trace files in the '
148                                            'perfetto ui.'))
149  open_parser.add_argument('file_path', help='Path to trace file.')
150  open_parser.add_argument('--use_trace_processor', default=False,
151                           action='store_true',
152                                  help=('Enables using trace_processor to open '
153                                        'the trace regardless of its size.'))
154
155  # Configure perfetto in virtualized Android
156  add_vm_parser(subparsers)
157
158  # Set 'profiler' as the default parser
159  parser.set_default_subparser('profiler')
160
161  return parser
162
163
164def user_changed_default_arguments(args):
165  return any([args.event != "custom",
166              args.profiler != "perfetto",
167              args.out_dir != DEFAULT_OUT_DIR,
168              args.dur_ms != DEFAULT_DUR_MS,
169              args.app is not None,
170              args.runs != 1,
171              args.simpleperf_event is not None,
172              args.perfetto_config != "default",
173              args.between_dur_ms != DEFAULT_DUR_MS,
174              args.ui is not None,
175              args.excluded_ftrace_events is not None,
176              args.included_ftrace_events is not None,
177              args.from_user is not None,
178              args.to_user is not None])
179
180def verify_profiler_args(args):
181  if args.out_dir != DEFAULT_OUT_DIR and not os.path.isdir(args.out_dir):
182    return None, ValidationError(
183        ("Command is invalid because --out-dir is not a valid directory"
184         " path: %s." % args.out_dir), None)
185
186  if args.dur_ms < MIN_DURATION_MS:
187    return None, ValidationError(
188        ("Command is invalid because --dur-ms cannot be set to a value smaller"
189         " than %d." % MIN_DURATION_MS),
190        ("Set --dur-ms %d to capture a trace for %d seconds."
191         % (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
192
193  if args.from_user is not None and args.event != "user-switch":
194    return None, ValidationError(
195        ("Command is invalid because --from-user is passed, but --event is not"
196         " set to user-switch."),
197        ("Set --event user-switch --from-user %s to perform a user-switch from"
198         " user %s." % (args.from_user, args.from_user)))
199
200  if args.to_user is not None and args.event != "user-switch":
201    return None, ValidationError((
202        "Command is invalid because --to-user is passed, but --event is not set"
203        " to user-switch."),
204        ("Set --event user-switch --to-user %s to perform a user-switch to user"
205         " %s." % (args.to_user, args.to_user)))
206
207  if args.event == "user-switch" and args.to_user is None:
208    return None, ValidationError(
209        "Command is invalid because --to-user is not passed.",
210        ("Set --event %s --to-user <user-id> to perform a %s."
211         % (args.event, args.event)))
212
213  # TODO(b/374313202): Support for simpleperf boot event will
214  #                    be added in the future
215  if args.event == "boot" and args.profiler == "simpleperf":
216    return None, ValidationError(
217        "Boot event is not yet implemented for simpleperf.",
218        "Please try another event.")
219
220  if args.app is not None and args.event != "app-startup":
221    return None, ValidationError(
222        ("Command is invalid because --app is passed and --event is not set"
223         " to app-startup."),
224        ("To profile an app startup run:"
225         " torq --event app-startup --app <package-name>"))
226
227  if args.event == "app-startup" and args.app is None:
228    return None, ValidationError(
229        "Command is invalid because --app is not passed.",
230        ("Set --event %s --app <package> to perform an %s."
231         % (args.event, args.event)))
232
233  if args.runs < 1:
234    return None, ValidationError(
235        ("Command is invalid because --runs cannot be set to a value smaller"
236         " than 1."), None)
237
238  if args.runs > 1 and args.ui:
239    return None, ValidationError(("Command is invalid because --ui cannot be"
240                                  " passed if --runs is set to a value greater"
241                                  " than 1."),
242                                 ("Set torq -r %d --no-ui to perform %d runs."
243                                  % (args.runs, args.runs)))
244
245  if args.simpleperf_event is not None and args.profiler != "simpleperf":
246    return None, ValidationError(
247        ("Command is invalid because --simpleperf-event cannot be passed"
248         " if --profiler is not set to simpleperf."),
249        ("To capture the simpleperf event run:"
250         " torq --profiler simpleperf --simpleperf-event %s"
251         % " --simpleperf-event ".join(args.simpleperf_event)))
252
253  if (args.simpleperf_event is not None and
254      len(args.simpleperf_event) != len(set(args.simpleperf_event))):
255    return None, ValidationError(
256        ("Command is invalid because redundant calls to --simpleperf-event"
257         " cannot be made."),
258        ("Only set --simpleperf-event cpu-cycles once if you want"
259         " to collect cpu-cycles."))
260
261  if args.perfetto_config != "default":
262    if args.profiler != "perfetto":
263      return None, ValidationError(
264          ("Command is invalid because --perfetto-config cannot be passed"
265           " if --profiler is not set to perfetto."),
266          ("Set --profiler perfetto to choose a perfetto-config"
267           " to use."))
268
269  if (args.perfetto_config not in PREDEFINED_PERFETTO_CONFIGS and
270      not os.path.isfile(args.perfetto_config)):
271    return None, ValidationError(
272        ("Command is invalid because --perfetto-config is not a valid"
273         " file path: %s" % args.perfetto_config),
274        ("Predefined perfetto configs can be used:\n"
275         "\t torq --perfetto-config %s\n"
276         "\t A filepath with a config can also be used:\n"
277         "\t torq --perfetto-config <config-filepath>"
278         % ("\n\t torq --perfetto-config"
279            " ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
280
281  if args.between_dur_ms < MIN_DURATION_MS:
282    return None, ValidationError(
283        ("Command is invalid because --between-dur-ms cannot be set to a"
284         " smaller value than %d." % MIN_DURATION_MS),
285        ("Set --between-dur-ms %d to wait %d seconds between"
286         " each run." % (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
287
288  if args.between_dur_ms != DEFAULT_DUR_MS and args.runs == 1:
289    return None, ValidationError(
290        ("Command is invalid because --between-dur-ms cannot be passed"
291         " if --runs is not a value greater than 1."),
292        "Set --runs 2 to run 2 tests.")
293
294  if args.excluded_ftrace_events is not None and args.profiler != "perfetto":
295    return None, ValidationError(
296        ("Command is invalid because --excluded-ftrace-events cannot be passed"
297         " if --profiler is not set to perfetto."),
298        ("Set --profiler perfetto to exclude an ftrace event"
299         " from perfetto config."))
300
301  if (args.excluded_ftrace_events is not None and
302      len(args.excluded_ftrace_events) != len(set(
303          args.excluded_ftrace_events))):
304    return None, ValidationError(
305        ("Command is invalid because duplicate ftrace events cannot be"
306         " included in --excluded-ftrace-events."),
307        ("--excluded-ftrace-events should only include one instance of an"
308         " ftrace event."))
309
310  if args.included_ftrace_events is not None and args.profiler != "perfetto":
311    return None, ValidationError(
312        ("Command is invalid because --included-ftrace-events cannot be passed"
313         " if --profiler is not set to perfetto."),
314        ("Set --profiler perfetto to include an ftrace event"
315         " in perfetto config."))
316
317  if (args.included_ftrace_events is not None and
318      len(args.included_ftrace_events) != len(set(
319          args.included_ftrace_events))):
320    return None, ValidationError(
321        ("Command is invalid because duplicate ftrace events cannot be"
322         " included in --included-ftrace-events."),
323        ("--included-ftrace-events should only include one instance of an"
324         " ftrace event."))
325
326  if (args.included_ftrace_events is not None and
327      args.excluded_ftrace_events is not None):
328    ftrace_event_intersection = sorted((set(args.excluded_ftrace_events) &
329                                        set(args.included_ftrace_events)))
330    if len(ftrace_event_intersection):
331      return None, ValidationError(
332          ("Command is invalid because ftrace event(s): %s cannot be both"
333           " included and excluded." % ", ".join(ftrace_event_intersection)),
334          ("\n\t ".join("Only set --excluded-ftrace-events %s if you want to"
335                        " exclude %s from the config or"
336                        " --included-ftrace-events %s if you want to include %s"
337                        " in the config."
338                        % (event, event, event, event)
339                        for event in ftrace_event_intersection)))
340
341  if args.profiler == "simpleperf" and args.simpleperf_event is None:
342    args.simpleperf_event = ['cpu-cycles']
343
344  if args.ui is None:
345    args.ui = args.runs == 1
346
347  if args.profiler == "simpleperf":
348    args, error = verify_simpleperf_args(args)
349    if error is not None:
350      return None, error
351  else:
352    args.scripts_path = None
353
354  return args, None
355
356def verify_config_args(args):
357  if args.config_subcommand is None:
358    return None, ValidationError(
359        ("Command is invalid because torq config cannot be called"
360         " without a subcommand."),
361        ("Use one of the following subcommands:\n"
362         "\t torq config list\n"
363         "\t torq config show\n"
364         "\t torq config pull\n"))
365
366  if args.config_subcommand == "pull":
367    if args.file_path is None:
368      args.file_path = "./" + args.config_name + ".pbtxt"
369    elif not os.path.isfile(args.file_path):
370      return None, ValidationError(
371          ("Command is invalid because %s is not a valid filepath."
372           % args.file_path),
373          ("A default filepath can be used if you do not specify a file-path:\n"
374           "\t torq pull default to copy to ./default.pbtxt\n"
375           "\t torq pull lightweight to copy to ./lightweight.pbtxt\n"
376           "\t torq pull memory to copy to ./memory.pbtxt"))
377
378  return args, None
379
380def verify_open_args(args):
381  if not path_exists(args.file_path):
382    return None, ValidationError(
383        "Command is invalid because %s is an invalid file path."
384        % args.file_path, "Make sure your file exists.")
385
386  return args, None
387
388def verify_args(args):
389  match args.subcommands:
390    case "profiler":
391      return verify_profiler_args(args)
392    case "config":
393      return verify_config_args(args)
394    case "open":
395      return verify_open_args(args)
396    case "vm":
397      return args, None
398    case _:
399      raise ValueError("Invalid command type used")
400
401def create_profiler_command(args):
402  return ProfilerCommand("profiler", args.event, args.profiler, args.out_dir,
403                         args.dur_ms,
404                         args.app, args.runs, args.simpleperf_event,
405                         args.perfetto_config, args.between_dur_ms,
406                         args.ui, args.excluded_ftrace_events,
407                         args.included_ftrace_events, args.from_user,
408                         args.to_user, args.scripts_path, args.symbols)
409
410
411def create_config_command(args):
412  type = "config " + args.config_subcommand
413  config_name = None
414  file_path = None
415  dur_ms = None
416  excluded_ftrace_events = None
417  included_ftrace_events = None
418  if args.config_subcommand == "pull" or args.config_subcommand == "show":
419    config_name = args.config_name
420    dur_ms = args.dur_ms
421    excluded_ftrace_events = args.excluded_ftrace_events
422    included_ftrace_events = args.included_ftrace_events
423    if args.config_subcommand == "pull":
424      file_path = args.file_path
425
426  command = ConfigCommand(type, config_name, file_path, dur_ms,
427      excluded_ftrace_events, included_ftrace_events)
428  return command
429
430
431def get_command_type(args):
432  match args.subcommands:
433    case "profiler":
434      return create_profiler_command(args)
435    case "config":
436      return create_config_command(args)
437    case "open":
438      return OpenCommand(args.file_path, args.use_trace_processor)
439    case "vm":
440      return create_vm_command(args)
441    case _:
442      raise ValueError("Invalid command type used")
443
444
445def print_error(error):
446  print(error.message)
447  if error.suggestion is not None:
448    print("Suggestion:\n\t", error.suggestion)
449
450
451def main():
452  parser = create_parser()
453  args = parser.parse_args()
454  args, error = verify_args(args)
455  if error is not None:
456    print_error(error)
457    return
458  command = get_command_type(args)
459  serial = args.serial[0] if args.serial else None
460  device = AdbDevice(serial)
461  error = command.execute(device)
462  if error is not None:
463    print_error(error)
464    return
465
466
467if __name__ == '__main__':
468  main()
469