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