xref: /aosp_15_r20/system/extras/torq/torq.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
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
24from validate_simpleperf import verify_simpleperf_args
25
26DEFAULT_DUR_MS = 10000
27MIN_DURATION_MS = 3000
28DEFAULT_OUT_DIR = "."
29
30
31def create_parser():
32  parser = argparse.ArgumentParser(prog='torq command',
33                                   description=('Torq CLI tool for performance'
34                                                ' tests.'))
35  parser.add_argument('-e', '--event',
36                      choices=['boot', 'user-switch', 'app-startup', 'custom'],
37                      default='custom', help='The event to trace/profile.')
38  parser.add_argument('-p', '--profiler', choices=['perfetto', 'simpleperf'],
39                      default='perfetto', help='The performance data source.')
40  parser.add_argument('-o', '--out-dir', default=DEFAULT_OUT_DIR,
41                      help='The path to the output directory.')
42  parser.add_argument('-d', '--dur-ms', type=int, default=DEFAULT_DUR_MS,
43                      help=('The duration (ms) of the event. Determines when'
44                            ' to stop collecting performance data.'))
45  parser.add_argument('-a', '--app',
46                      help='The package name of the app we want to start.')
47  parser.add_argument('-r', '--runs', type=int, default=1,
48                      help=('The number of times to run the event and'
49                            ' capture the perf data.'))
50  parser.add_argument('-s', '--simpleperf-event', action='append',
51                      help=('Simpleperf supported events to be collected.'
52                            ' e.g. cpu-cycles, instructions'))
53  parser.add_argument('--perfetto-config', default='default',
54                      help=('Predefined perfetto configs can be used:'
55                            ' %s. A filepath with a custom config could'
56                            ' also be provided.'
57                            % (", ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
58  parser.add_argument('--between-dur-ms', type=int, default=DEFAULT_DUR_MS,
59                      help='Time (ms) to wait before executing the next event.')
60  parser.add_argument('--ui', action=argparse.BooleanOptionalAction,
61                      help=('Specifies opening of UI visualization tool'
62                            ' after profiling is complete.'))
63  parser.add_argument('--excluded-ftrace-events', action='append',
64                      help=('Excludes specified ftrace event from the perfetto'
65                            ' config events.'))
66  parser.add_argument('--included-ftrace-events', action='append',
67                      help=('Includes specified ftrace event in the perfetto'
68                            ' config events.'))
69  parser.add_argument('--from-user', type=int,
70                      help='The user id from which to start the user switch')
71  parser.add_argument('--to-user', type=int,
72                      help='The user id of user that system is switching to.')
73  parser.add_argument('--serial',
74                      help=(('Specifies serial of the device that will be'
75                             ' used.')))
76  parser.add_argument('--symbols',
77                      help='Specifies path to symbols library.')
78  subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
79  config_parser = subparsers.add_parser('config',
80                                        help=('The config subcommand used'
81                                              ' to list and show the'
82                                              ' predefined perfetto configs.'))
83  config_subparsers = config_parser.add_subparsers(dest='config_subcommand',
84                                                   help=('torq config'
85                                                         ' subcommands'))
86  config_subparsers.add_parser('list',
87                               help=('Command to list the predefined'
88                                     ' perfetto configs'))
89  config_show_parser = config_subparsers.add_parser('show',
90                                                    help=('Command to print'
91                                                          ' the '
92                                                          ' perfetto config'
93                                                          ' in the terminal.'))
94  config_show_parser.add_argument('config_name',
95                                  choices=['lightweight', 'default', 'memory'],
96                                  help=('Name of the predefined perfetto'
97                                        ' config to print.'))
98  config_pull_parser = config_subparsers.add_parser('pull',
99                                                    help=('Command to copy'
100                                                          ' a predefined config'
101                                                          ' to the specified'
102                                                          ' file path.'))
103  config_pull_parser.add_argument('config_name',
104                                  choices=['lightweight', 'default', 'memory'],
105                                  help='Name of the predefined config to copy')
106  config_pull_parser.add_argument('file_path', nargs='?',
107                                  help=('File path to copy the predefined'
108                                        ' config to'))
109  open_parser = subparsers.add_parser('open',
110                                      help=('The open subcommand is used '
111                                            'to open trace files in the '
112                                            'perfetto ui.'))
113  open_parser.add_argument('file_path', help='Path to trace file.')
114  return parser
115
116
117def user_changed_default_arguments(args):
118  return any([args.event != "custom",
119              args.profiler != "perfetto",
120              args.out_dir != DEFAULT_OUT_DIR,
121              args.dur_ms != DEFAULT_DUR_MS,
122              args.app is not None,
123              args.runs != 1,
124              args.simpleperf_event is not None,
125              args.perfetto_config != "default",
126              args.between_dur_ms != DEFAULT_DUR_MS,
127              args.ui is not None,
128              args.excluded_ftrace_events is not None,
129              args.included_ftrace_events is not None,
130              args.from_user is not None,
131              args.to_user is not None,
132              args.serial is not None])
133
134
135def verify_args(args):
136  if (args.subcommands is not None and
137      user_changed_default_arguments(args)):
138    return None, ValidationError(
139        ("Command is invalid because profiler command is followed by a config"
140         " command."),
141        "Remove the 'config' subcommand to profile the device instead.")
142
143  if args.out_dir != DEFAULT_OUT_DIR and not os.path.isdir(args.out_dir):
144    return None, ValidationError(
145        ("Command is invalid because --out-dir is not a valid directory"
146         " path: %s." % args.out_dir), None)
147
148  if args.dur_ms < MIN_DURATION_MS:
149    return None, ValidationError(
150        ("Command is invalid because --dur-ms cannot be set to a value smaller"
151         " than %d." % MIN_DURATION_MS),
152        ("Set --dur-ms %d to capture a trace for %d seconds."
153         % (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
154
155  if args.from_user is not None and args.event != "user-switch":
156    return None, ValidationError(
157        ("Command is invalid because --from-user is passed, but --event is not"
158         " set to user-switch."),
159        ("Set --event user-switch --from-user %s to perform a user-switch from"
160         " user %s." % (args.from_user, args.from_user)))
161
162  if args.to_user is not None and args.event != "user-switch":
163    return None, ValidationError((
164        "Command is invalid because --to-user is passed, but --event is not set"
165        " to user-switch."),
166        ("Set --event user-switch --to-user %s to perform a user-switch to user"
167         " %s." % (args.to_user, args.to_user)))
168
169  if args.event == "user-switch" and args.to_user is None:
170    return None, ValidationError(
171        "Command is invalid because --to-user is not passed.",
172        ("Set --event %s --to-user <user-id> to perform a %s."
173         % (args.event, args.event)))
174
175  # TODO(b/374313202): Support for simpleperf boot event will
176  #                    be added in the future
177  if args.event == "boot" and args.profiler == "simpleperf":
178    return None, ValidationError(
179        "Boot event is not yet implemented for simpleperf.",
180        "Please try another event.")
181
182  if args.app is not None and args.event != "app-startup":
183    return None, ValidationError(
184        ("Command is invalid because --app is passed and --event is not set"
185         " to app-startup."),
186        ("To profile an app startup run:"
187         " torq --event app-startup --app <package-name>"))
188
189  if args.event == "app-startup" and args.app is None:
190    return None, ValidationError(
191        "Command is invalid because --app is not passed.",
192        ("Set --event %s --app <package> to perform an %s."
193         % (args.event, args.event)))
194
195  if args.runs < 1:
196    return None, ValidationError(
197        ("Command is invalid because --runs cannot be set to a value smaller"
198         " than 1."), None)
199
200  if args.runs > 1 and args.ui:
201    return None, ValidationError(("Command is invalid because --ui cannot be"
202                                  " passed if --runs is set to a value greater"
203                                  " than 1."),
204                                 ("Set torq -r %d --no-ui to perform %d runs."
205                                  % (args.runs, args.runs)))
206
207  if args.simpleperf_event is not None and args.profiler != "simpleperf":
208    return None, ValidationError(
209        ("Command is invalid because --simpleperf-event cannot be passed"
210         " if --profiler is not set to simpleperf."),
211        ("To capture the simpleperf event run:"
212         " torq --profiler simpleperf --simpleperf-event %s"
213         % " --simpleperf-event ".join(args.simpleperf_event)))
214
215  if (args.simpleperf_event is not None and
216      len(args.simpleperf_event) != len(set(args.simpleperf_event))):
217    return None, ValidationError(
218        ("Command is invalid because redundant calls to --simpleperf-event"
219         " cannot be made."),
220        ("Only set --simpleperf-event cpu-cycles once if you want"
221         " to collect cpu-cycles."))
222
223  if args.perfetto_config != "default":
224    if args.profiler != "perfetto":
225      return None, ValidationError(
226          ("Command is invalid because --perfetto-config cannot be passed"
227           " if --profiler is not set to perfetto."),
228          ("Set --profiler perfetto to choose a perfetto-config"
229           " to use."))
230
231  if (args.perfetto_config not in PREDEFINED_PERFETTO_CONFIGS and
232      not os.path.isfile(args.perfetto_config)):
233    return None, ValidationError(
234        ("Command is invalid because --perfetto-config is not a valid"
235         " file path: %s" % args.perfetto_config),
236        ("Predefined perfetto configs can be used:\n"
237         "\t torq --perfetto-config %s\n"
238         "\t A filepath with a config can also be used:\n"
239         "\t torq --perfetto-config <config-filepath>"
240         % ("\n\t torq --perfetto-config"
241            " ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
242
243  if args.between_dur_ms < MIN_DURATION_MS:
244    return None, ValidationError(
245        ("Command is invalid because --between-dur-ms cannot be set to a"
246         " smaller value than %d." % MIN_DURATION_MS),
247        ("Set --between-dur-ms %d to wait %d seconds between"
248         " each run." % (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
249
250  if args.between_dur_ms != DEFAULT_DUR_MS and args.runs == 1:
251    return None, ValidationError(
252        ("Command is invalid because --between-dur-ms cannot be passed"
253         " if --runs is not a value greater than 1."),
254        "Set --runs 2 to run 2 tests.")
255
256  if args.excluded_ftrace_events is not None and args.profiler != "perfetto":
257    return None, ValidationError(
258        ("Command is invalid because --excluded-ftrace-events cannot be passed"
259         " if --profiler is not set to perfetto."),
260        ("Set --profiler perfetto to exclude an ftrace event"
261         " from perfetto config."))
262
263  if (args.excluded_ftrace_events is not None and
264      len(args.excluded_ftrace_events) != len(set(
265          args.excluded_ftrace_events))):
266    return None, ValidationError(
267        ("Command is invalid because duplicate ftrace events cannot be"
268         " included in --excluded-ftrace-events."),
269        ("--excluded-ftrace-events should only include one instance of an"
270         " ftrace event."))
271
272  if args.included_ftrace_events is not None and args.profiler != "perfetto":
273    return None, ValidationError(
274        ("Command is invalid because --included-ftrace-events cannot be passed"
275         " if --profiler is not set to perfetto."),
276        ("Set --profiler perfetto to include an ftrace event"
277         " in perfetto config."))
278
279  if (args.included_ftrace_events is not None and
280      len(args.included_ftrace_events) != len(set(
281          args.included_ftrace_events))):
282    return None, ValidationError(
283        ("Command is invalid because duplicate ftrace events cannot be"
284         " included in --included-ftrace-events."),
285        ("--included-ftrace-events should only include one instance of an"
286         " ftrace event."))
287
288  if (args.included_ftrace_events is not None and
289      args.excluded_ftrace_events is not None):
290    ftrace_event_intersection = sorted((set(args.excluded_ftrace_events) &
291                                        set(args.included_ftrace_events)))
292    if len(ftrace_event_intersection):
293      return None, ValidationError(
294          ("Command is invalid because ftrace event(s): %s cannot be both"
295           " included and excluded." % ", ".join(ftrace_event_intersection)),
296          ("\n\t ".join("Only set --excluded-ftrace-events %s if you want to"
297                        " exclude %s from the config or"
298                        " --included-ftrace-events %s if you want to include %s"
299                        " in the config."
300                        % (event, event, event, event)
301                        for event in ftrace_event_intersection)))
302
303  if args.subcommands == "config" and args.config_subcommand is None:
304    return None, ValidationError(
305        ("Command is invalid because torq config cannot be called"
306         " without a subcommand."),
307        ("Use one of the following subcommands:\n"
308         "\t torq config list\n"
309         "\t torq config show\n"
310         "\t torq config pull\n"))
311
312  if args.profiler == "simpleperf" and args.simpleperf_event is None:
313    args.simpleperf_event = ['cpu-cycles']
314
315  if args.ui is None:
316    args.ui = args.runs == 1
317
318  if args.subcommands == "config" and args.config_subcommand == "pull":
319    if args.file_path is None:
320      args.file_path = "./" + args.config_name + ".pbtxt"
321    elif not os.path.isfile(args.file_path):
322      return None, ValidationError(
323          ("Command is invalid because %s is not a valid filepath."
324           % args.file_path),
325          ("A default filepath can be used if you do not specify a file-path:\n"
326           "\t torq pull default to copy to ./default.pbtxt\n"
327           "\t torq pull lightweight to copy to ./lightweight.pbtxt\n"
328           "\t torq pull memory to copy to ./memory.pbtxt"))
329
330  if args.subcommands == "open" and not path_exists(args.file_path):
331    return None, ValidationError(
332        "Command is invalid because %s is an invalid file path."
333        % args.file_path, "Make sure your file exists.")
334
335  if args.profiler == "simpleperf":
336    args, error = verify_simpleperf_args(args)
337    if error is not None:
338      return None, error
339  else:
340    args.scripts_path = None
341
342  return args, None
343
344
345def create_profiler_command(args):
346  return ProfilerCommand("profiler", args.event, args.profiler, args.out_dir,
347                         args.dur_ms,
348                         args.app, args.runs, args.simpleperf_event,
349                         args.perfetto_config, args.between_dur_ms,
350                         args.ui, args.excluded_ftrace_events,
351                         args.included_ftrace_events, args.from_user,
352                         args.to_user)
353
354
355def create_config_command(args):
356  type = "config " + args.config_subcommand
357  config_name = None
358  file_path = None
359  dur_ms = None
360  excluded_ftrace_events = None
361  included_ftrace_events = None
362  if args.config_subcommand == "pull" or args.config_subcommand == "show":
363    config_name = args.config_name
364    dur_ms = args.dur_ms
365    excluded_ftrace_events = args.excluded_ftrace_events
366    included_ftrace_events = args.included_ftrace_events
367    if args.config_subcommand == "pull":
368      file_path = args.file_path
369
370  command = ConfigCommand(type, config_name, file_path, dur_ms,
371      excluded_ftrace_events, included_ftrace_events)
372  return command
373
374
375def get_command_type(args):
376  command = None
377  if args.subcommands is None:
378    command = create_profiler_command(args)
379  if args.subcommands == "config":
380    command = create_config_command(args)
381  if args.subcommands == "open":
382    command = OpenCommand(args.file_path)
383  return command
384
385
386def print_error(error):
387  print(error.message)
388  if error.suggestion is not None:
389    print("Suggestion:\n\t", error.suggestion)
390
391
392def main():
393  parser = create_parser()
394  args = parser.parse_args()
395  args, error = verify_args(args)
396  if error is not None:
397    print_error(error)
398    return
399  command = get_command_type(args)
400  device = AdbDevice(args.serial)
401  error = command.execute(device)
402  if error is not None:
403    print_error(error)
404    return
405
406
407if __name__ == '__main__':
408  main()
409