blob: 97874b18ef9f06d42f057a9a94f69aa5ac355e0c [file] [log] [blame]
#!/usr/bin/python3
# Copyright (C) 2022 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.
#
"""Tool to analyze CPU performance with some cores disabled.
Should install perfetto: $ pip install perfetto
"""
import argparse
import os
import subprocess
import sys
from config import CpuSettings as CpuSettings
from config import get_script_dir as get_script_dir
from config import parse_config as parse_config
from config import parse_ints as parse_ints
from perfetto_cpu_analysis import run_analysis as run_analysis
ADB_CMD = "adb"
def init_arguments():
parser = argparse.ArgumentParser(description='Analyze CPU perf.')
parser.add_argument('-f', '--configfile', dest='config_file',
default=get_script_dir() + '/pixel6.config', type=argparse.FileType('r'),
help='CPU config file', )
parser.add_argument('-c', '--cpusettings', dest='cpusettings', action='store',
default='default',
help='CPU Settings to apply')
parser.add_argument('-s', '--serial', dest='serial', action='store',
help='android device serial number')
parser.add_argument('-w', '--waittime', dest='waittime', action='store',
help='wait for up to this time in secs to after CPU settings change.' +\
' Default is to wait forever until user press any key')
parser.add_argument('-l', '--perfetto_tool_location', dest='perfetto_tool_location',
action='store', default='./external/perfetto/tools',
help='Location of perfotto/tool directory.' +\
' Default is ./external/perfetto/tools.')
parser.add_argument('-o', '--perfetto_output', dest='perfetto_output', action='store',
help='Output trace file for perfetto. If this is not specified' +\
', perfetto tracing will not run.')
parser.add_argument('-t', '--traceduration', dest='traceduration', action='store',
default='5',
help='duration of trace capturing. Default is 5 sec.')
parser.add_argument('-p', '--permanent', dest='permanent',
action='store_true',
default=False,
help='change CPU settings permanently and do not restore original setting')
return parser.parse_args()
def run_adb_cmd(cmd):
r = subprocess.check_output(ADB_CMD + ' ' + cmd, shell=True)
return r.decode("utf-8")
def run_adb_shell_cmd(cmd):
return run_adb_cmd('shell ' + cmd)
def run_shell_cmd(cmd):
return subprocess.check_output(cmd, shell=True)
def read_device_cpusets():
# needs '' to keep command complete through adb shell
r = run_adb_shell_cmd("'find /dev/cpuset -name cpus -print -exec cat {} \;'")
lines = r.split('\n')
key = None
sets = {}
for l in lines:
l = l.strip()
if l.find("/dev/cpuset/") == 0:
l = l.replace("/dev/cpuset/", "")
l = l.replace("cpus", "")
l = l.replace("/", "")
key = l
elif key is not None:
cores = parse_ints(l)
sets[key] = cores
key = None
return sets
def read_device_governors():
governors = {}
r = run_adb_shell_cmd("'find /sys/devices/system/cpu/cpufreq -name scaling_governor" +\
" -print -exec cat {} \;'")
lines = r.split('\n')
key = None
for l in lines:
l = l.strip()
if l.find("/sys/devices/system/cpu/cpufreq/") == 0:
l = l.replace("/sys/devices/system/cpu/cpufreq/", "")
l = l.replace("/scaling_governor", "")
key = l
elif key is not None:
governors[key] = str(l)
key = None
return governors
def read_device_cpu_settings():
settings = CpuSettings()
settings.cpusets = read_device_cpusets()
settings.onlines = parse_ints(run_adb_shell_cmd("cat /sys/devices/system/cpu/online"))
offline_cores = parse_ints(run_adb_shell_cmd("cat /sys/devices/system/cpu/offline"))
settings.allcores.extend(settings.onlines)
settings.allcores.extend(offline_cores)
settings.allcores.sort()
settings.governors = read_device_governors()
return settings
def wait_for_user_input(msg):
return input(msg)
def get_cores_to_offline(settings, deviceSettings = None):
allcores = []
allcores.extend(settings.allcores)
if deviceSettings is not None:
for core in deviceSettings.allcores:
if not core in allcores:
allcores.append(core)
allcores.sort()
for core in settings.onlines:
allcores.remove(core) # remove online cores
return allcores
def write_sysfs(node, contents):
run_adb_shell_cmd("chmod 666 {}".format(node))
run_adb_shell_cmd("\"echo '{}' > {}\"".format(contents, node))
def enable_disable_cores(onlines, offlines):
for core in onlines:
write_sysfs("/sys/devices/system/cpu/cpu{}/online".format(core), '1')
for core in offlines:
write_sysfs("/sys/devices/system/cpu/cpu{}/online".format(core), '0')
def update_cpusets(settings, offlines, deviceSettings):
cpusets = {}
if deviceSettings is not None:
for k in deviceSettings.cpusets:
cpusets[k] = deviceSettings.cpusets[k].copy()
for k in settings.cpusets:
if deviceSettings is not None:
if not k in deviceSettings.cpusets:
print("CPUSet {} not existing in device, ignore".format(k))
continue
cpusets[k] = settings.cpusets[k].copy()
for k in cpusets:
if k == "": # special case, no need to touch
continue
cores = cpusets[k]
for core in offlines:
if core in cores:
cores.remove(core)
cores_string = []
for core in cores:
cores_string.append(str(core))
write_sysfs("/dev/cpuset/{}/cpus".format(k), ','.join(cores_string))
def update_policies(settings, deviceSettings = None):
policies = {}
if len(settings.governors) == 0:
# at least governor should be set
for k in deviceSettings.governors:
policies[k] = settings.governor
else:
policies = settings.governors
print("Setting policies:{}".format(policies))
for k in policies:
try:
write_sysfs("/sys/devices/system/cpu/cpufreq/{}/scaling_governor".\
format(k), policies[k])
except subprocess.CalledProcessError as e:
# policies can be gone when all cpus are gone
print("Cannot set policy {}".format(k))
def apply_cpu_settings(settings, deviceSettings = None):
print("Apply settings:{}".format(settings))
offlines = get_cores_to_offline(settings, deviceSettings)
print("Cores going offline:{}".format(offlines))
update_cpusets(settings, offlines, deviceSettings)
# change cores
enable_disable_cores(settings.onlines, offlines)
# change policies
update_policies(settings, deviceSettings)
def main():
global ADB_CMD
args = init_arguments()
if args.serial is not None:
ADB_CMD = "%s %s" % ("adb -s", args.serial)
print("config file:{}".format(args.config_file.name))
run_adb_cmd('root')
# parse config
cpuConfig = parse_config(args.config_file)
print("*Config:\n" + str(cpuConfig))
# read CPU settings
deviceSettings = read_device_cpu_settings();
print("*Current device CPU settings:\n" + str(deviceSettings))
# change CPU setting
newCpuSettings = cpuConfig.configs.get(args.cpusettings)
if newCpuSettings is None:
print("Cannot find cpusettings {}".format(args.cpusettings))
apply_cpu_settings(newCpuSettings, deviceSettings)
# wait
if args.waittime is None:
wait_for_user_input("Presse enter to start capturing perfetto trace")
else:
print ("Wait {} secs before capturing perfetto trace".format(args.waittime))
sleep(int(args.waittime))
if args.perfetto_output is not None:
# run perfetto & analysis if output file is specified
serial_str = ""
if args.serial is not None:
serial_str = "--serial {}".format(args.serial)
outputName = args.perfetto_output
if not outputName.endswith('.pftrace'):
outputName = outputName + '.pftrace'
print("Starting capture to {}".format(outputName))
r = run_shell_cmd("{}/record_android_trace {} -t {}s -o {} sched freq idle".\
format(args.perfetto_tool_location, serial_str, args.traceduration, outputName))
print(r)
# analysis
run_analysis(outputName, cpuConfig, newCpuSettings)
# restore
if not args.permanent:
apply_cpu_settings(deviceSettings)
if __name__ == '__main__':
main()