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