267 lines
9.2 KiB
Python
267 lines
9.2 KiB
Python
#!/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"
|
|
CPU_FREQ_GOVERNOR = [
|
|
"conservative",
|
|
"ondemand",
|
|
"performance",
|
|
"powersave",
|
|
"schedutil",
|
|
"userspace",
|
|
]
|
|
|
|
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 perfetto/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')
|
|
parser.add_argument('-g', '--governor', dest='governor', action='store',
|
|
choices=CPU_FREQ_GOVERNOR,
|
|
help='CPU governor to apply, overrides the CPU Settings')
|
|
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("Applying CPU Settings:\n" + 30 * "-")
|
|
print(str(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)
|
|
if args.governor is not None:
|
|
for k in cpuConfig.configs:
|
|
cpuConfig.configs[k].governor = args.governor
|
|
print("CONFIG:\n" + 30 * "-" + "\n" + str(cpuConfig))
|
|
|
|
# read CPU settings
|
|
deviceSettings = read_device_cpu_settings();
|
|
print("Current device CPU settings:\n" + 30 * "-" + "\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("Press 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()
|