blob: 5f1b6ed61f18b16a9ef72cb655fa4ef5c2f45923 [file] [log] [blame]
#
# Copyright (C) 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import argparse
import os
from command import ProfilerCommand, ConfigCommand, OpenCommand
from device import AdbDevice
from validation_error import ValidationError
from config_builder import PREDEFINED_PERFETTO_CONFIGS
from utils import path_exists
from validate_simpleperf import verify_simpleperf_args
DEFAULT_DUR_MS = 10000
MIN_DURATION_MS = 3000
DEFAULT_OUT_DIR = "."
def create_parser():
parser = argparse.ArgumentParser(prog='torq command',
description=('Torq CLI tool for performance'
' tests.'))
parser.add_argument('-e', '--event',
choices=['boot', 'user-switch', 'app-startup', 'custom'],
default='custom', help='The event to trace/profile.')
parser.add_argument('-p', '--profiler', choices=['perfetto', 'simpleperf'],
default='perfetto', help='The performance data source.')
parser.add_argument('-o', '--out-dir', default=DEFAULT_OUT_DIR,
help='The path to the output directory.')
parser.add_argument('-d', '--dur-ms', type=int, default=DEFAULT_DUR_MS,
help=('The duration (ms) of the event. Determines when'
' to stop collecting performance data.'))
parser.add_argument('-a', '--app',
help='The package name of the app we want to start.')
parser.add_argument('-r', '--runs', type=int, default=1,
help=('The number of times to run the event and'
' capture the perf data.'))
parser.add_argument('-s', '--simpleperf-event', action='append',
help=('Simpleperf supported events to be collected.'
' e.g. cpu-cycles, instructions'))
parser.add_argument('--perfetto-config', default='default',
help=('Predefined perfetto configs can be used:'
' %s. A filepath with a custom config could'
' also be provided.'
% (", ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
parser.add_argument('--between-dur-ms', type=int, default=DEFAULT_DUR_MS,
help='Time (ms) to wait before executing the next event.')
parser.add_argument('--ui', action=argparse.BooleanOptionalAction,
help=('Specifies opening of UI visualization tool'
' after profiling is complete.'))
parser.add_argument('--excluded-ftrace-events', action='append',
help=('Excludes specified ftrace event from the perfetto'
' config events.'))
parser.add_argument('--included-ftrace-events', action='append',
help=('Includes specified ftrace event in the perfetto'
' config events.'))
parser.add_argument('--from-user', type=int,
help='The user id from which to start the user switch')
parser.add_argument('--to-user', type=int,
help='The user id of user that system is switching to.')
parser.add_argument('--serial',
help=(('Specifies serial of the device that will be'
' used.')))
parser.add_argument('--symbols',
help='Specifies path to symbols library.')
subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
config_parser = subparsers.add_parser('config',
help=('The config subcommand used'
' to list and show the'
' predefined perfetto configs.'))
config_subparsers = config_parser.add_subparsers(dest='config_subcommand',
help=('torq config'
' subcommands'))
config_subparsers.add_parser('list',
help=('Command to list the predefined'
' perfetto configs'))
config_show_parser = config_subparsers.add_parser('show',
help=('Command to print'
' the '
' perfetto config'
' in the terminal.'))
config_show_parser.add_argument('config_name',
choices=['lightweight', 'default', 'memory'],
help=('Name of the predefined perfetto'
' config to print.'))
config_pull_parser = config_subparsers.add_parser('pull',
help=('Command to copy'
' a predefined config'
' to the specified'
' file path.'))
config_pull_parser.add_argument('config_name',
choices=['lightweight', 'default', 'memory'],
help='Name of the predefined config to copy')
config_pull_parser.add_argument('file_path', nargs='?',
help=('File path to copy the predefined'
' config to'))
open_parser = subparsers.add_parser('open',
help=('The open subcommand is used '
'to open trace files in the '
'perfetto ui.'))
open_parser.add_argument('file_path', help='Path to trace file.')
return parser
def user_changed_default_arguments(args):
return any([args.event != "custom",
args.profiler != "perfetto",
args.out_dir != DEFAULT_OUT_DIR,
args.dur_ms != DEFAULT_DUR_MS,
args.app is not None,
args.runs != 1,
args.simpleperf_event is not None,
args.perfetto_config != "default",
args.between_dur_ms != DEFAULT_DUR_MS,
args.ui is not None,
args.excluded_ftrace_events is not None,
args.included_ftrace_events is not None,
args.from_user is not None,
args.to_user is not None,
args.serial is not None])
def verify_args(args):
if (args.subcommands is not None and
user_changed_default_arguments(args)):
return None, ValidationError(
("Command is invalid because profiler command is followed by a config"
" command."),
"Remove the 'config' subcommand to profile the device instead.")
if args.out_dir != DEFAULT_OUT_DIR and not os.path.isdir(args.out_dir):
return None, ValidationError(
("Command is invalid because --out-dir is not a valid directory"
" path: %s." % args.out_dir), None)
if args.dur_ms < MIN_DURATION_MS:
return None, ValidationError(
("Command is invalid because --dur-ms cannot be set to a value smaller"
" than %d." % MIN_DURATION_MS),
("Set --dur-ms %d to capture a trace for %d seconds."
% (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
if args.from_user is not None and args.event != "user-switch":
return None, ValidationError(
("Command is invalid because --from-user is passed, but --event is not"
" set to user-switch."),
("Set --event user-switch --from-user %s to perform a user-switch from"
" user %s." % (args.from_user, args.from_user)))
if args.to_user is not None and args.event != "user-switch":
return None, ValidationError((
"Command is invalid because --to-user is passed, but --event is not set"
" to user-switch."),
("Set --event user-switch --to-user %s to perform a user-switch to user"
" %s." % (args.to_user, args.to_user)))
if args.event == "user-switch" and args.to_user is None:
return None, ValidationError(
"Command is invalid because --to-user is not passed.",
("Set --event %s --to-user <user-id> to perform a %s."
% (args.event, args.event)))
# TODO(b/374313202): Support for simpleperf boot event will
# be added in the future
if args.event == "boot" and args.profiler == "simpleperf":
return None, ValidationError(
"Boot event is not yet implemented for simpleperf.",
"Please try another event.")
if args.app is not None and args.event != "app-startup":
return None, ValidationError(
("Command is invalid because --app is passed and --event is not set"
" to app-startup."),
("To profile an app startup run:"
" torq --event app-startup --app <package-name>"))
if args.event == "app-startup" and args.app is None:
return None, ValidationError(
"Command is invalid because --app is not passed.",
("Set --event %s --app <package> to perform an %s."
% (args.event, args.event)))
if args.runs < 1:
return None, ValidationError(
("Command is invalid because --runs cannot be set to a value smaller"
" than 1."), None)
if args.runs > 1 and args.ui:
return None, ValidationError(("Command is invalid because --ui cannot be"
" passed if --runs is set to a value greater"
" than 1."),
("Set torq -r %d --no-ui to perform %d runs."
% (args.runs, args.runs)))
if args.simpleperf_event is not None and args.profiler != "simpleperf":
return None, ValidationError(
("Command is invalid because --simpleperf-event cannot be passed"
" if --profiler is not set to simpleperf."),
("To capture the simpleperf event run:"
" torq --profiler simpleperf --simpleperf-event %s"
% " --simpleperf-event ".join(args.simpleperf_event)))
if (args.simpleperf_event is not None and
len(args.simpleperf_event) != len(set(args.simpleperf_event))):
return None, ValidationError(
("Command is invalid because redundant calls to --simpleperf-event"
" cannot be made."),
("Only set --simpleperf-event cpu-cycles once if you want"
" to collect cpu-cycles."))
if args.perfetto_config != "default":
if args.profiler != "perfetto":
return None, ValidationError(
("Command is invalid because --perfetto-config cannot be passed"
" if --profiler is not set to perfetto."),
("Set --profiler perfetto to choose a perfetto-config"
" to use."))
if (args.perfetto_config not in PREDEFINED_PERFETTO_CONFIGS and
not os.path.isfile(args.perfetto_config)):
return None, ValidationError(
("Command is invalid because --perfetto-config is not a valid"
" file path: %s" % args.perfetto_config),
("Predefined perfetto configs can be used:\n"
"\t torq --perfetto-config %s\n"
"\t A filepath with a config can also be used:\n"
"\t torq --perfetto-config <config-filepath>"
% ("\n\t torq --perfetto-config"
" ".join(PREDEFINED_PERFETTO_CONFIGS.keys()))))
if args.between_dur_ms < MIN_DURATION_MS:
return None, ValidationError(
("Command is invalid because --between-dur-ms cannot be set to a"
" smaller value than %d." % MIN_DURATION_MS),
("Set --between-dur-ms %d to wait %d seconds between"
" each run." % (MIN_DURATION_MS, (MIN_DURATION_MS / 1000))))
if args.between_dur_ms != DEFAULT_DUR_MS and args.runs == 1:
return None, ValidationError(
("Command is invalid because --between-dur-ms cannot be passed"
" if --runs is not a value greater than 1."),
"Set --runs 2 to run 2 tests.")
if args.excluded_ftrace_events is not None and args.profiler != "perfetto":
return None, ValidationError(
("Command is invalid because --excluded-ftrace-events cannot be passed"
" if --profiler is not set to perfetto."),
("Set --profiler perfetto to exclude an ftrace event"
" from perfetto config."))
if (args.excluded_ftrace_events is not None and
len(args.excluded_ftrace_events) != len(set(
args.excluded_ftrace_events))):
return None, ValidationError(
("Command is invalid because duplicate ftrace events cannot be"
" included in --excluded-ftrace-events."),
("--excluded-ftrace-events should only include one instance of an"
" ftrace event."))
if args.included_ftrace_events is not None and args.profiler != "perfetto":
return None, ValidationError(
("Command is invalid because --included-ftrace-events cannot be passed"
" if --profiler is not set to perfetto."),
("Set --profiler perfetto to include an ftrace event"
" in perfetto config."))
if (args.included_ftrace_events is not None and
len(args.included_ftrace_events) != len(set(
args.included_ftrace_events))):
return None, ValidationError(
("Command is invalid because duplicate ftrace events cannot be"
" included in --included-ftrace-events."),
("--included-ftrace-events should only include one instance of an"
" ftrace event."))
if (args.included_ftrace_events is not None and
args.excluded_ftrace_events is not None):
ftrace_event_intersection = sorted((set(args.excluded_ftrace_events) &
set(args.included_ftrace_events)))
if len(ftrace_event_intersection):
return None, ValidationError(
("Command is invalid because ftrace event(s): %s cannot be both"
" included and excluded." % ", ".join(ftrace_event_intersection)),
("\n\t ".join("Only set --excluded-ftrace-events %s if you want to"
" exclude %s from the config or"
" --included-ftrace-events %s if you want to include %s"
" in the config."
% (event, event, event, event)
for event in ftrace_event_intersection)))
if args.subcommands == "config" and args.config_subcommand is None:
return None, ValidationError(
("Command is invalid because torq config cannot be called"
" without a subcommand."),
("Use one of the following subcommands:\n"
"\t torq config list\n"
"\t torq config show\n"
"\t torq config pull\n"))
if args.profiler == "simpleperf" and args.simpleperf_event is None:
args.simpleperf_event = ['cpu-cycles']
if args.ui is None:
args.ui = args.runs == 1
if args.subcommands == "config" and args.config_subcommand == "pull":
if args.file_path is None:
args.file_path = "./" + args.config_name + ".pbtxt"
elif not os.path.isfile(args.file_path):
return None, ValidationError(
("Command is invalid because %s is not a valid filepath."
% args.file_path),
("A default filepath can be used if you do not specify a file-path:\n"
"\t torq pull default to copy to ./default.pbtxt\n"
"\t torq pull lightweight to copy to ./lightweight.pbtxt\n"
"\t torq pull memory to copy to ./memory.pbtxt"))
if args.subcommands == "open" and not path_exists(args.file_path):
return None, ValidationError(
"Command is invalid because %s is an invalid file path."
% args.file_path, "Make sure your file exists.")
if args.profiler == "simpleperf":
args, error = verify_simpleperf_args(args)
if error is not None:
return None, error
else:
args.scripts_path = None
return args, None
def create_profiler_command(args):
return ProfilerCommand("profiler", args.event, args.profiler, args.out_dir,
args.dur_ms,
args.app, args.runs, args.simpleperf_event,
args.perfetto_config, args.between_dur_ms,
args.ui, args.excluded_ftrace_events,
args.included_ftrace_events, args.from_user,
args.to_user, args.scripts_path, args.symbols)
def create_config_command(args):
type = "config " + args.config_subcommand
config_name = None
file_path = None
dur_ms = None
excluded_ftrace_events = None
included_ftrace_events = None
if args.config_subcommand == "pull" or args.config_subcommand == "show":
config_name = args.config_name
dur_ms = args.dur_ms
excluded_ftrace_events = args.excluded_ftrace_events
included_ftrace_events = args.included_ftrace_events
if args.config_subcommand == "pull":
file_path = args.file_path
command = ConfigCommand(type, config_name, file_path, dur_ms,
excluded_ftrace_events, included_ftrace_events)
return command
def get_command_type(args):
command = None
if args.subcommands is None:
command = create_profiler_command(args)
if args.subcommands == "config":
command = create_config_command(args)
if args.subcommands == "open":
command = OpenCommand(args.file_path)
return command
def print_error(error):
print(error.message)
if error.suggestion is not None:
print("Suggestion:\n\t", error.suggestion)
def main():
parser = create_parser()
args = parser.parse_args()
args, error = verify_args(args)
if error is not None:
print_error(error)
return
command = get_command_type(args)
device = AdbDevice(args.serial)
error = command.execute(device)
if error is not None:
print_error(error)
return
if __name__ == '__main__':
main()