• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import atexit
6import logging
7import re
8
9from devil.android import device_errors
10
11logger = logging.getLogger(__name__)
12_atexit_messages = set()
13
14
15# Defines how to switch between the default performance configuration
16# ('default_mode') and the mode for use when benchmarking ('high_perf_mode').
17# For devices not in the list the defaults are to set up the scaling governor to
18# 'performance' and reset it back to 'ondemand' when benchmarking is finished.
19#
20# The 'default_mode_governor' is mandatory to define, while
21# 'high_perf_mode_governor' is not taken into account. The latter is because the
22# governor 'performance' is currently used for all benchmarking on all devices.
23#
24# TODO(crbug.com/383566): Add definitions for all devices used in the perf
25# waterfall.
26_PERFORMANCE_MODE_DEFINITIONS = {
27  # Fire TV Edition - 4K
28  'AFTKMST12': {
29    'default_mode_governor': 'interactive',
30  },
31  'GT-I9300': {
32    'default_mode_governor': 'pegasusq',
33  },
34  'Galaxy Nexus': {
35    'default_mode_governor': 'interactive',
36  },
37  'Nexus 7': {
38    'default_mode_governor': 'interactive',
39  },
40  'Nexus 10': {
41    'default_mode_governor': 'interactive',
42  },
43  'Nexus 4': {
44    'high_perf_mode': {
45      'bring_cpu_cores_online': True,
46    },
47    'default_mode_governor': 'ondemand',
48  },
49  'Nexus 5': {
50    # The list of possible GPU frequency values can be found in:
51    #     /sys/class/kgsl/kgsl-3d0/gpu_available_frequencies.
52    # For CPU cores the possible frequency values are at:
53    #     /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
54    'high_perf_mode': {
55      'bring_cpu_cores_online': True,
56      'cpu_max_freq': 1190400,
57      'gpu_max_freq': 200000000,
58    },
59    'default_mode': {
60      'cpu_max_freq': 2265600,
61      'gpu_max_freq': 450000000,
62    },
63    'default_mode_governor': 'ondemand',
64  },
65  'Nexus 5X': {
66    'high_perf_mode': {
67      'bring_cpu_cores_online': True,
68      'cpu_max_freq': 1248000,
69      'gpu_max_freq': 300000000,
70    },
71    'default_mode': {
72      'governor': 'ondemand',
73      # The SoC is ARM big.LITTLE. The cores 4..5 are big, the 0..3 are LITTLE.
74      'cpu_max_freq': {'0..3': 1440000, '4..5': 1824000},
75      'gpu_max_freq': 600000000,
76    },
77    'default_mode_governor': 'ondemand',
78  },
79}
80
81
82def _NoisyWarning(message):
83  message += ' Results may be NOISY!!'
84  logger.warning(message)
85  # Add an additional warning at exit, such that it's clear that any results
86  # may be different/noisy (due to the lack of intended performance mode).
87  if message not in _atexit_messages:
88    _atexit_messages.add(message)
89    atexit.register(logger.warning, message)
90
91
92class PerfControl(object):
93  """Provides methods for setting the performance mode of a device."""
94
95  _AVAILABLE_GOVERNORS_REL_PATH = 'cpufreq/scaling_available_governors'
96  _CPU_FILE_PATTERN = re.compile(r'^cpu\d+$')
97  _CPU_PATH = '/sys/devices/system/cpu'
98  _KERNEL_MAX = '/sys/devices/system/cpu/kernel_max'
99
100  def __init__(self, device):
101    self._device = device
102    self._cpu_files = []
103    for file_name in self._device.ListDirectory(self._CPU_PATH, as_root=True):
104      if self._CPU_FILE_PATTERN.match(file_name):
105        self._cpu_files.append(file_name)
106    assert self._cpu_files, 'Failed to detect CPUs.'
107    self._cpu_file_list = ' '.join(self._cpu_files)
108    logger.info('CPUs found: %s', self._cpu_file_list)
109
110    self._have_mpdecision = self._device.FileExists('/system/bin/mpdecision')
111
112    raw = self._ReadEachCpuFile(self._AVAILABLE_GOVERNORS_REL_PATH)
113    self._available_governors = [
114        (cpu, raw_governors.strip().split() if not exit_code else None)
115        for cpu, raw_governors, exit_code in raw]
116
117  def _SetMaxFrequenciesFromMode(self, mode):
118    """Set maximum frequencies for GPU and CPU cores.
119
120    Args:
121      mode: A dictionary mapping optional keys 'cpu_max_freq' and 'gpu_max_freq'
122            to integer values of frequency supported by the device.
123    """
124    cpu_max_freq = mode.get('cpu_max_freq')
125    if cpu_max_freq:
126      if not isinstance(cpu_max_freq, dict):
127        self._SetScalingMaxFreqForCpus(cpu_max_freq, self._cpu_file_list)
128      else:
129        for key, max_frequency in cpu_max_freq.iteritems():
130          # Convert 'X' to 'cpuX' and 'X..Y' to 'cpuX cpu<X+1> .. cpuY'.
131          if '..' in key:
132            range_min, range_max = key.split('..')
133            range_min, range_max = int(range_min), int(range_max)
134          else:
135            range_min = range_max = int(key)
136          cpu_files = ['cpu%d' % number
137                       for number in xrange(range_min, range_max + 1)]
138          # Set the |max_frequency| on requested subset of the cores.
139          self._SetScalingMaxFreqForCpus(max_frequency, ' '.join(cpu_files))
140    gpu_max_freq = mode.get('gpu_max_freq')
141    if gpu_max_freq:
142      self._SetMaxGpuClock(gpu_max_freq)
143
144  def SetHighPerfMode(self):
145    """Sets the highest stable performance mode for the device."""
146    try:
147      self._device.EnableRoot()
148    except device_errors.CommandFailedError:
149      _NoisyWarning('Need root for performance mode.')
150      return
151    mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
152        self._device.product_model)
153    if not mode_definitions:
154      self.SetScalingGovernor('performance')
155      return
156    high_perf_mode = mode_definitions.get('high_perf_mode')
157    if not high_perf_mode:
158      self.SetScalingGovernor('performance')
159      return
160    if high_perf_mode.get('bring_cpu_cores_online', False):
161      self._ForceAllCpusOnline(True)
162      if not self._AllCpusAreOnline():
163        _NoisyWarning('Failed to force CPUs online.')
164    # Scaling governor must be set _after_ bringing all CPU cores online,
165    # otherwise it would not affect the cores that are currently offline.
166    self.SetScalingGovernor('performance')
167    self._SetMaxFrequenciesFromMode(high_perf_mode)
168
169  def SetDefaultPerfMode(self):
170    """Sets the performance mode for the device to its default mode."""
171    if not self._device.HasRoot():
172      return
173    mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
174        self._device.product_model)
175    if not mode_definitions:
176      self.SetScalingGovernor('ondemand')
177    else:
178      default_mode_governor = mode_definitions.get('default_mode_governor')
179      assert default_mode_governor, ('Default mode governor must be provided '
180          'for all perf mode definitions.')
181      self.SetScalingGovernor(default_mode_governor)
182      default_mode = mode_definitions.get('default_mode')
183      if default_mode:
184        self._SetMaxFrequenciesFromMode(default_mode)
185    self._ForceAllCpusOnline(False)
186
187  def SetPerfProfilingMode(self):
188    """Enables all cores for reliable perf profiling."""
189    self._ForceAllCpusOnline(True)
190    self.SetScalingGovernor('performance')
191    if not self._AllCpusAreOnline():
192      if not self._device.HasRoot():
193        raise RuntimeError('Need root to force CPUs online.')
194      raise RuntimeError('Failed to force CPUs online.')
195
196  def GetCpuInfo(self):
197    online = (output.rstrip() == '1' and status == 0
198              for (_, output, status) in self._ForEachCpu('cat "$CPU/online"'))
199    governor = (output.rstrip() if status == 0 else None
200                for (_, output, status)
201                in self._ForEachCpu('cat "$CPU/cpufreq/scaling_governor"'))
202    return zip(self._cpu_files, online, governor)
203
204  def _ForEachCpu(self, cmd, cpu_list=None):
205    """Runs a command on the device for each of the CPUs.
206
207    Args:
208      cmd: A string with a shell command, may may use shell expansion: "$CPU" to
209           refer to the current CPU in the string form (e.g. "cpu0", "cpu1",
210           and so on).
211      cpu_list: A space-separated string of CPU core names, like in the example
212           above
213    Returns:
214      A list of tuples in the form (cpu_string, command_output, exit_code), one
215      tuple per each command invocation. As usual, all lines of the output
216      command are joined into one line with spaces.
217    """
218    if cpu_list is None:
219      cpu_list = self._cpu_file_list
220    script = '; '.join([
221        'for CPU in %s' % cpu_list,
222        'do %s' % cmd,
223        'echo -n "%~%$?%~%"',
224        'done'
225    ])
226    output = self._device.RunShellCommand(
227        script, cwd=self._CPU_PATH, check_return=True, as_root=True, shell=True)
228    output = '\n'.join(output).split('%~%')
229    return zip(self._cpu_files, output[0::2], (int(c) for c in output[1::2]))
230
231  def _ConditionallyWriteCpuFiles(self, path, value, cpu_files, condition):
232    template = (
233        '{condition} && test -e "$CPU/{path}" && echo {value} > "$CPU/{path}"')
234    results = self._ForEachCpu(
235        template.format(path=path, value=value, condition=condition), cpu_files)
236    cpus = ' '.join(cpu for (cpu, _, status) in results if status == 0)
237    if cpus:
238      logger.info('Successfully set %s to %r on: %s', path, value, cpus)
239    else:
240      logger.warning('Failed to set %s to %r on any cpus', path, value)
241
242  def _WriteCpuFiles(self, path, value, cpu_files):
243    self._ConditionallyWriteCpuFiles(path, value, cpu_files, condition='true')
244
245  def _ReadEachCpuFile(self, path):
246    return self._ForEachCpu(
247        'cat "$CPU/{path}"'.format(path=path))
248
249  def SetScalingGovernor(self, value):
250    """Sets the scaling governor to the given value on all possible CPUs.
251
252    This does not attempt to set a governor to a value not reported as available
253    on the corresponding CPU.
254
255    Args:
256      value: [string] The new governor value.
257    """
258    condition = 'test -e "{path}" && grep -q {value} {path}'.format(
259        path=('${CPU}/%s' % self._AVAILABLE_GOVERNORS_REL_PATH),
260        value=value)
261    self._ConditionallyWriteCpuFiles(
262        'cpufreq/scaling_governor', value, self._cpu_file_list, condition)
263
264  def GetScalingGovernor(self):
265    """Gets the currently set governor for each CPU.
266
267    Returns:
268      An iterable of 2-tuples, each containing the cpu and the current
269      governor.
270    """
271    raw = self._ReadEachCpuFile('cpufreq/scaling_governor')
272    return [
273        (cpu, raw_governor.strip() if not exit_code else None)
274        for cpu, raw_governor, exit_code in raw]
275
276  def ListAvailableGovernors(self):
277    """Returns the list of available governors for each CPU.
278
279    Returns:
280      An iterable of 2-tuples, each containing the cpu and a list of available
281      governors for that cpu.
282    """
283    return self._available_governors
284
285  def _SetScalingMaxFreqForCpus(self, value, cpu_files):
286    self._WriteCpuFiles('cpufreq/scaling_max_freq', '%d' % value, cpu_files)
287
288  def _SetMaxGpuClock(self, value):
289    self._device.WriteFile('/sys/class/kgsl/kgsl-3d0/max_gpuclk',
290                           str(value),
291                           as_root=True)
292
293  def _AllCpusAreOnline(self):
294    results = self._ForEachCpu('cat "$CPU/online"')
295    # The file 'cpu0/online' is missing on some devices (example: Nexus 9). This
296    # is likely because on these devices it is impossible to bring the cpu0
297    # offline. Assuming the same for all devices until proven otherwise.
298    return all(output.rstrip() == '1' and status == 0
299               for (cpu, output, status) in results
300               if cpu != 'cpu0')
301
302  def _ForceAllCpusOnline(self, force_online):
303    """Enable all CPUs on a device.
304
305    Some vendors (or only Qualcomm?) hot-plug their CPUs, which can add noise
306    to measurements:
307    - In perf, samples are only taken for the CPUs that are online when the
308      measurement is started.
309    - The scaling governor can't be set for an offline CPU and frequency scaling
310      on newly enabled CPUs adds noise to both perf and tracing measurements.
311
312    It appears Qualcomm is the only vendor that hot-plugs CPUs, and on Qualcomm
313    this is done by "mpdecision".
314
315    """
316    if self._have_mpdecision:
317      cmd = ['stop', 'mpdecision'] if force_online else ['start', 'mpdecision']
318      self._device.RunShellCommand(cmd, check_return=True, as_root=True)
319
320    if not self._have_mpdecision and not self._AllCpusAreOnline():
321      logger.warning('Unexpected cpu hot plugging detected.')
322
323    if force_online:
324      self._ForEachCpu('echo 1 > "$CPU/online"')
325