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