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