1#!/usr/bin/python3 2 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17"""Tool to analyze CPU performance with some cores disabled. 18Should install perfetto: $ pip install perfetto 19""" 20 21import argparse 22import os 23import subprocess 24import sys 25 26from config import CpuSettings as CpuSettings 27from config import get_script_dir as get_script_dir 28from config import parse_config as parse_config 29from config import parse_ints as parse_ints 30from perfetto_cpu_analysis import run_analysis as run_analysis 31 32ADB_CMD = "adb" 33CPU_FREQ_GOVERNOR = [ 34 "conservative", 35 "ondemand", 36 "performance", 37 "powersave", 38 "schedutil", 39 "userspace", 40] 41 42def init_arguments(): 43 parser = argparse.ArgumentParser(description='Analyze CPU perf.') 44 parser.add_argument('-f', '--configfile', dest='config_file', 45 default=get_script_dir() + '/pixel6.config', type=argparse.FileType('r'), 46 help='CPU config file', ) 47 parser.add_argument('-c', '--cpusettings', dest='cpusettings', action='store', 48 default='default', 49 help='CPU Settings to apply') 50 parser.add_argument('-s', '--serial', dest='serial', action='store', 51 help='android device serial number') 52 parser.add_argument('-w', '--waittime', dest='waittime', action='store', 53 help='wait for up to this time in secs to after CPU settings change.' +\ 54 ' Default is to wait forever until user press any key') 55 parser.add_argument('-l', '--perfetto_tool_location', dest='perfetto_tool_location', 56 action='store', default='./external/perfetto/tools', 57 help='Location of perfetto/tool directory.' +\ 58 ' Default is ./external/perfetto/tools.') 59 parser.add_argument('-o', '--perfetto_output', dest='perfetto_output', action='store', 60 help='Output trace file for perfetto. If this is not specified' +\ 61 ', perfetto tracing will not run.') 62 parser.add_argument('-t', '--traceduration', dest='traceduration', action='store', 63 default='5', 64 help='duration of trace capturing. Default is 5 sec.') 65 parser.add_argument('-p', '--permanent', dest='permanent', 66 action='store_true', 67 default=False, 68 help='change CPU settings permanently and do not restore original setting') 69 parser.add_argument('-g', '--governor', dest='governor', action='store', 70 choices=CPU_FREQ_GOVERNOR, 71 help='CPU governor to apply, overrides the CPU Settings') 72 return parser.parse_args() 73 74def run_adb_cmd(cmd): 75 r = subprocess.check_output(ADB_CMD + ' ' + cmd, shell=True) 76 return r.decode("utf-8") 77 78def run_adb_shell_cmd(cmd): 79 return run_adb_cmd('shell ' + cmd) 80 81def run_shell_cmd(cmd): 82 return subprocess.check_output(cmd, shell=True) 83 84def read_device_cpusets(): 85 # needs '' to keep command complete through adb shell 86 r = run_adb_shell_cmd("'find /dev/cpuset -name cpus -print -exec cat {} \;'") 87 lines = r.split('\n') 88 key = None 89 sets = {} 90 for l in lines: 91 l = l.strip() 92 if l.find("/dev/cpuset/") == 0: 93 l = l.replace("/dev/cpuset/", "") 94 l = l.replace("cpus", "") 95 l = l.replace("/", "") 96 key = l 97 elif key is not None: 98 cores = parse_ints(l) 99 sets[key] = cores 100 key = None 101 return sets 102 103def read_device_governors(): 104 governors = {} 105 r = run_adb_shell_cmd("'find /sys/devices/system/cpu/cpufreq -name scaling_governor" +\ 106 " -print -exec cat {} \;'") 107 lines = r.split('\n') 108 key = None 109 for l in lines: 110 l = l.strip() 111 if l.find("/sys/devices/system/cpu/cpufreq/") == 0: 112 l = l.replace("/sys/devices/system/cpu/cpufreq/", "") 113 l = l.replace("/scaling_governor", "") 114 key = l 115 elif key is not None: 116 governors[key] = str(l) 117 key = None 118 return governors 119 120def read_device_cpu_settings(): 121 settings = CpuSettings() 122 123 settings.cpusets = read_device_cpusets() 124 125 settings.onlines = parse_ints(run_adb_shell_cmd("cat /sys/devices/system/cpu/online")) 126 offline_cores = parse_ints(run_adb_shell_cmd("cat /sys/devices/system/cpu/offline")) 127 settings.allcores.extend(settings.onlines) 128 settings.allcores.extend(offline_cores) 129 settings.allcores.sort() 130 settings.governors = read_device_governors() 131 132 return settings 133 134def wait_for_user_input(msg): 135 return input(msg) 136 137def get_cores_to_offline(settings, deviceSettings = None): 138 allcores = [] 139 allcores.extend(settings.allcores) 140 if deviceSettings is not None: 141 for core in deviceSettings.allcores: 142 if not core in allcores: 143 allcores.append(core) 144 allcores.sort() 145 for core in settings.onlines: 146 allcores.remove(core) # remove online cores 147 return allcores 148 149def write_sysfs(node, contents): 150 run_adb_shell_cmd("chmod 666 {}".format(node)) 151 run_adb_shell_cmd("\"echo '{}' > {}\"".format(contents, node)) 152 153def enable_disable_cores(onlines, offlines): 154 for core in onlines: 155 write_sysfs("/sys/devices/system/cpu/cpu{}/online".format(core), '1') 156 for core in offlines: 157 write_sysfs("/sys/devices/system/cpu/cpu{}/online".format(core), '0') 158 159def update_cpusets(settings, offlines, deviceSettings): 160 cpusets = {} 161 if deviceSettings is not None: 162 for k in deviceSettings.cpusets: 163 cpusets[k] = deviceSettings.cpusets[k].copy() 164 for k in settings.cpusets: 165 if deviceSettings is not None: 166 if not k in deviceSettings.cpusets: 167 print("CPUSet {} not existing in device, ignore".format(k)) 168 continue 169 cpusets[k] = settings.cpusets[k].copy() 170 for k in cpusets: 171 if k == "": # special case, no need to touch 172 continue 173 cores = cpusets[k] 174 for core in offlines: 175 if core in cores: 176 cores.remove(core) 177 cores_string = [] 178 for core in cores: 179 cores_string.append(str(core)) 180 write_sysfs("/dev/cpuset/{}/cpus".format(k), ','.join(cores_string)) 181 182def update_policies(settings, deviceSettings = None): 183 policies = {} 184 if len(settings.governors) == 0: 185 # at least governor should be set 186 for k in deviceSettings.governors: 187 policies[k] = settings.governor 188 else: 189 policies = settings.governors 190 191 print("Setting policies:{}".format(policies)) 192 for k in policies: 193 try: 194 write_sysfs("/sys/devices/system/cpu/cpufreq/{}/scaling_governor".\ 195 format(k), policies[k]) 196 except subprocess.CalledProcessError as e: 197 # policies can be gone when all cpus are gone 198 print("Cannot set policy {}".format(k)) 199 200def apply_cpu_settings(settings, deviceSettings = None): 201 print("Applying CPU Settings:\n" + 30 * "-") 202 print(str(settings)) 203 offlines = get_cores_to_offline(settings, deviceSettings) 204 print("Cores going offline:{}".format(offlines)) 205 206 update_cpusets(settings, offlines, deviceSettings) 207 # change cores 208 enable_disable_cores(settings.onlines, offlines) 209 # change policies 210 update_policies(settings, deviceSettings) 211 212 213def main(): 214 global ADB_CMD 215 args = init_arguments() 216 if args.serial is not None: 217 ADB_CMD = "%s %s" % ("adb -s", args.serial) 218 219 print("config file:{}".format(args.config_file.name)) 220 run_adb_cmd('root') 221 222 # parse config 223 cpuConfig = parse_config(args.config_file) 224 if args.governor is not None: 225 for k in cpuConfig.configs: 226 cpuConfig.configs[k].governor = args.governor 227 print("CONFIG:\n" + 30 * "-" + "\n" + str(cpuConfig)) 228 229 # read CPU settings 230 deviceSettings = read_device_cpu_settings(); 231 print("Current device CPU settings:\n" + 30 * "-" + "\n" + str(deviceSettings)) 232 233 # change CPU setting 234 newCpuSettings = cpuConfig.configs.get(args.cpusettings) 235 if newCpuSettings is None: 236 print("Cannot find cpusettings {}".format(args.cpusettings)) 237 apply_cpu_settings(newCpuSettings, deviceSettings) 238 239 # wait 240 if args.waittime is None: 241 wait_for_user_input("Press enter to start capturing perfetto trace") 242 else: 243 print ("Wait {} secs before capturing perfetto trace".format(args.waittime)) 244 sleep(int(args.waittime)) 245 246 if args.perfetto_output is not None: 247 # run perfetto & analysis if output file is specified 248 serial_str = "" 249 if args.serial is not None: 250 serial_str = "--serial {}".format(args.serial) 251 outputName = args.perfetto_output 252 if not outputName.endswith('.pftrace'): 253 outputName = outputName + '.pftrace' 254 print("Starting capture to {}".format(outputName)) 255 r = run_shell_cmd("{}/record_android_trace {} -t {}s -o {} sched freq idle".\ 256 format(args.perfetto_tool_location, serial_str, args.traceduration, outputName)) 257 print(r) 258 # analysis 259 run_analysis(outputName, cpuConfig, newCpuSettings) 260 261 # restore 262 if not args.permanent: 263 apply_cpu_settings(deviceSettings) 264 265if __name__ == '__main__': 266 main() 267