1 # Copyright 2013 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import atexit 6 import logging 7 import re 8 9 from devil.android import device_errors 10 11 logger = logging.getLogger(__name__) 12 13 14 class PerfControl(object): 15 """Provides methods for setting the performance mode of a device.""" 16 17 _AVAILABLE_GOVERNORS_REL_PATH = 'cpufreq/scaling_available_governors' 18 _CPU_FILE_PATTERN = re.compile(r'^cpu\d+$') 19 _CPU_PATH = '/sys/devices/system/cpu' 20 _KERNEL_MAX = '/sys/devices/system/cpu/kernel_max' 21 22 def __init__(self, device): 23 self._device = device 24 self._cpu_files = [ 25 filename 26 for filename in self._device.ListDirectory(self._CPU_PATH, as_root=True) 27 if self._CPU_FILE_PATTERN.match(filename)] 28 assert self._cpu_files, 'Failed to detect CPUs.' 29 self._cpu_file_list = ' '.join(self._cpu_files) 30 logger.info('CPUs found: %s', self._cpu_file_list) 31 32 self._have_mpdecision = self._device.FileExists('/system/bin/mpdecision') 33 34 raw = self._ReadEachCpuFile(self._AVAILABLE_GOVERNORS_REL_PATH) 35 self._available_governors = [ 36 (cpu, raw_governors.strip().split() if not exit_code else None) 37 for cpu, raw_governors, exit_code in raw] 38 39 def SetHighPerfMode(self): 40 """Sets the highest stable performance mode for the device.""" 41 try: 42 self._device.EnableRoot() 43 except device_errors.CommandFailedError: 44 message = 'Need root for performance mode. Results may be NOISY!!' 45 logger.warning(message) 46 # Add an additional warning at exit, such that it's clear that any results 47 # may be different/noisy (due to the lack of intended performance mode). 48 atexit.register(logger.warning, message) 49 return 50 51 product_model = self._device.product_model 52 # TODO(epenner): Enable on all devices (http://crbug.com/383566) 53 if 'Nexus 4' == product_model: 54 self._ForceAllCpusOnline(True) 55 if not self._AllCpusAreOnline(): 56 logger.warning('Failed to force CPUs online. Results may be NOISY!') 57 self.SetScalingGovernor('performance') 58 elif 'Nexus 5' == product_model: 59 self._ForceAllCpusOnline(True) 60 if not self._AllCpusAreOnline(): 61 logger.warning('Failed to force CPUs online. Results may be NOISY!') 62 self.SetScalingGovernor('performance') 63 self._SetScalingMaxFreq(1190400) 64 self._SetMaxGpuClock(200000000) 65 else: 66 self.SetScalingGovernor('performance') 67 68 def SetPerfProfilingMode(self): 69 """Enables all cores for reliable perf profiling.""" 70 self._ForceAllCpusOnline(True) 71 self.SetScalingGovernor('performance') 72 if not self._AllCpusAreOnline(): 73 if not self._device.HasRoot(): 74 raise RuntimeError('Need root to force CPUs online.') 75 raise RuntimeError('Failed to force CPUs online.') 76 77 def SetDefaultPerfMode(self): 78 """Sets the performance mode for the device to its default mode.""" 79 if not self._device.HasRoot(): 80 return 81 product_model = self._device.product_model 82 if 'Nexus 5' == product_model: 83 if self._AllCpusAreOnline(): 84 self._SetScalingMaxFreq(2265600) 85 self._SetMaxGpuClock(450000000) 86 87 governor_mode = { 88 'GT-I9300': 'pegasusq', 89 'Galaxy Nexus': 'interactive', 90 'Nexus 4': 'ondemand', 91 'Nexus 5': 'ondemand', 92 'Nexus 7': 'interactive', 93 'Nexus 10': 'interactive' 94 }.get(product_model, 'ondemand') 95 self.SetScalingGovernor(governor_mode) 96 self._ForceAllCpusOnline(False) 97 98 def GetCpuInfo(self): 99 online = (output.rstrip() == '1' and status == 0 100 for (_, output, status) in self._ForEachCpu('cat "$CPU/online"')) 101 governor = (output.rstrip() if status == 0 else None 102 for (_, output, status) 103 in self._ForEachCpu('cat "$CPU/cpufreq/scaling_governor"')) 104 return zip(self._cpu_files, online, governor) 105 106 def _ForEachCpu(self, cmd): 107 script = '; '.join([ 108 'for CPU in %s' % self._cpu_file_list, 109 'do %s' % cmd, 110 'echo -n "%~%$?%~%"', 111 'done' 112 ]) 113 output = self._device.RunShellCommand( 114 script, cwd=self._CPU_PATH, check_return=True, as_root=True, shell=True) 115 output = '\n'.join(output).split('%~%') 116 return zip(self._cpu_files, output[0::2], (int(c) for c in output[1::2])) 117 118 def _WriteEachCpuFile(self, path, value): 119 self._ConditionallyWriteEachCpuFile(path, value, condition='true') 120 121 def _ConditionallyWriteEachCpuFile(self, path, value, condition): 122 template = ( 123 '{condition} && test -e "$CPU/{path}" && echo {value} > "$CPU/{path}"') 124 results = self._ForEachCpu( 125 template.format(path=path, value=value, condition=condition)) 126 cpus = ' '.join(cpu for (cpu, _, status) in results if status == 0) 127 if cpus: 128 logger.info('Successfully set %s to %r on: %s', path, value, cpus) 129 else: 130 logger.warning('Failed to set %s to %r on any cpus', path, value) 131 132 def _ReadEachCpuFile(self, path): 133 return self._ForEachCpu( 134 'cat "$CPU/{path}"'.format(path=path)) 135 136 def SetScalingGovernor(self, value): 137 """Sets the scaling governor to the given value on all possible CPUs. 138 139 This does not attempt to set a governor to a value not reported as available 140 on the corresponding CPU. 141 142 Args: 143 value: [string] The new governor value. 144 """ 145 condition = 'test -e "{path}" && grep -q {value} {path}'.format( 146 path=('${CPU}/%s' % self._AVAILABLE_GOVERNORS_REL_PATH), 147 value=value) 148 self._ConditionallyWriteEachCpuFile( 149 'cpufreq/scaling_governor', value, condition) 150 151 def GetScalingGovernor(self): 152 """Gets the currently set governor for each CPU. 153 154 Returns: 155 An iterable of 2-tuples, each containing the cpu and the current 156 governor. 157 """ 158 raw = self._ReadEachCpuFile('cpufreq/scaling_governor') 159 return [ 160 (cpu, raw_governor.strip() if not exit_code else None) 161 for cpu, raw_governor, exit_code in raw] 162 163 def ListAvailableGovernors(self): 164 """Returns the list of available governors for each CPU. 165 166 Returns: 167 An iterable of 2-tuples, each containing the cpu and a list of available 168 governors for that cpu. 169 """ 170 return self._available_governors 171 172 def _SetScalingMaxFreq(self, value): 173 self._WriteEachCpuFile('cpufreq/scaling_max_freq', '%d' % value) 174 175 def _SetMaxGpuClock(self, value): 176 self._device.WriteFile('/sys/class/kgsl/kgsl-3d0/max_gpuclk', 177 str(value), 178 as_root=True) 179 180 def _AllCpusAreOnline(self): 181 results = self._ForEachCpu('cat "$CPU/online"') 182 # TODO(epenner): Investigate why file may be missing 183 # (http://crbug.com/397118) 184 return all(output.rstrip() == '1' and status == 0 185 for (cpu, output, status) in results 186 if cpu != 'cpu0') 187 188 def _ForceAllCpusOnline(self, force_online): 189 """Enable all CPUs on a device. 190 191 Some vendors (or only Qualcomm?) hot-plug their CPUs, which can add noise 192 to measurements: 193 - In perf, samples are only taken for the CPUs that are online when the 194 measurement is started. 195 - The scaling governor can't be set for an offline CPU and frequency scaling 196 on newly enabled CPUs adds noise to both perf and tracing measurements. 197 198 It appears Qualcomm is the only vendor that hot-plugs CPUs, and on Qualcomm 199 this is done by "mpdecision". 200 201 """ 202 if self._have_mpdecision: 203 cmd = ['stop', 'mpdecision'] if force_online else ['start', 'mpdecision'] 204 self._device.RunShellCommand(cmd, check_return=True, as_root=True) 205 206 if not self._have_mpdecision and not self._AllCpusAreOnline(): 207 logger.warning('Unexpected cpu hot plugging detected.') 208 209 if force_online: 210 self._ForEachCpu('echo 1 > "$CPU/online"') 211