blob: c666a8da25eb5eaa48bee53a6557553721996fef [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, HWCommand, ConfigCommand
from device import AdbDevice
from validation_error import ValidationError
from config_builder import PREDEFINED_PERFETTO_CONFIGS
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.')))
subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
hw_parser = subparsers.add_parser('hw',
help=('The hardware subcommand used to'
' change the H/W configuration of'
' the device.'))
hw_subparsers = hw_parser.add_subparsers(dest='hw_subcommand',
help='torq hw subcommands')
hw_set_parser = hw_subparsers.add_parser('set',
help=('Command to set a new'
' hardware configuration'))
hw_set_parser.add_argument('hw_set_config', nargs='?',
choices=['seahawk', 'seaturtle'],
help='Pre-defined hardware configuration')
hw_set_parser.add_argument('-n', '--num-cpus', type=int,
help='The amount of active cores in the hardware.')
hw_set_parser.add_argument('-m', '--memory',
help=('The memory limit the device would have.'
' E.g. 4G'))
hw_subparsers.add_parser('get',
help=('Command to get the current hardware'
' configuration. Will provide the number of'
' cpus and memory available.'))
hw_subparsers.add_parser('list',
help=('Command to list the supported HW'
' configurations.'))
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'))
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 hw"
" or config command."),
"Remove the 'hw' or '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)))
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 == "hw" and args.hw_subcommand is None:
return None, ValidationError(
("Command is invalid because torq hw cannot be called without"
" a subcommand."),
("Use one of the following subcommands:\n"
"\t torq hw set <config-name>\n"
"\t torq hw get\n"
"\t torq hw list"))
if (args.subcommands == "hw" and args.hw_subcommand == "set" and
args.hw_set_config is not None and args.num_cpus is not None):
return None, ValidationError(
("Command is invalid because torq hw --num-cpus cannot be passed if a"
" new hardware configuration is also set at the same time"),
("Set torq hw --num-cpus 2 by itself to set 2 active"
" cores in the hardware."))
if (args.subcommands == "hw" and args.hw_subcommand == "set" and
args.hw_set_config is not None and args.memory is not None):
return None, ValidationError(
("Command is invalid because torq hw --memory cannot be passed if a"
" new hardware configuration is also set at the same time"),
("Set torq hw --memory 4G by itself to limit the memory"
" of the device to 4 gigabytes."))
if (args.subcommands == "hw" and args.hw_subcommand == "set" and
args.num_cpus is not None and args.num_cpus < 1):
return None, ValidationError(
("Command is invalid because hw set --num-cpus cannot be set to"
" smaller than 1."),
("Set hw set --num-cpus 1 to set 1 active core in"
" hardware."))
if (args.subcommands == "hw" and args.hw_subcommand == "set" and
args.memory is not None):
index = args.memory.find("G")
if index == -1 or args.memory[-1] != "G" or len(args.memory) == 1:
return None, ValidationError(
("Command is invalid because the argument for hw set --memory does"
" not match the <int>G format."),
("Set hw set --memory 4G to limit the memory of the"
" device to 4 gigabytes."))
for i in range(index):
if not args.memory[i].isdigit():
return None, ValidationError(
("Command is invalid because the argument for hw set --memory"
" does not match the <int>G format."),
("Set hw set --memory 4G to limit the memory of"
" the device to 4 gigabytes."))
if args.memory[0] == "0":
return None, ValidationError(
("Command is invalid because hw set --memory cannot be set to"
" smaller than 1."),
("Set hw set --memory 4G to limit the memory of"
" the device to 4 gigabytes."))
if (args.subcommands == "hw" and args.hw_subcommand == "set" and
args.hw_set_config is None and args.num_cpus is None and
args.memory is None):
return None, ValidationError(
("Command is invalid because torq hw set cannot be called without"
" a subcommand."),
("Use one of the following subcommands:\n"
"\t (torq hw set <config>, torq hw set --num-cpus <int>,\n"
"\t torq hw set --memory <int>G,\n"
"\t torq hw set --num-cpus <int> --memory <int>G,\n"
"\t torq hw set --memory <int>G --num-cpus <int>)"))
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" and
args.file_path is None):
args.file_path = "./" + args.config_name + ".pbtxt"
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)
def create_hw_command(args):
command = None
type = "hw " + args.hw_subcommand
if args.hw_subcommand == "set":
command = HWCommand(type, args.hw_set_config, args.num_cpus,
args.memory)
else:
command = HWCommand(type, None, None, None)
return command
def create_config_command(args):
command = None
type = "config " + args.config_subcommand
if args.config_subcommand == "pull":
command = ConfigCommand(type, args.config_name, args.file_path)
if args.config_subcommand == "show":
command = ConfigCommand(type, args.config_name, None)
if args.config_subcommand == "list":
command = ConfigCommand(type, None, None)
return command
def get_command_type(args):
command = None
if args.subcommands is None:
command = create_profiler_command(args)
if args.subcommands == "hw":
command = create_hw_command(args)
if args.subcommands == "config":
command = create_config_command(args)
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()