| #!/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() |