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
7
8from devil.android import device_errors
9
10
11class PerfControl(object):
12  """Provides methods for setting the performance mode of a device."""
13  _CPU_PATH = '/sys/devices/system/cpu'
14  _KERNEL_MAX = '/sys/devices/system/cpu/kernel_max'
15
16  def __init__(self, device):
17    self._device = device
18    # this will raise an AdbCommandFailedError if no CPU files are found
19    self._cpu_files = self._device.RunShellCommand(
20        'ls -d cpu[0-9]*', cwd=self._CPU_PATH, check_return=True, as_root=True)
21    assert self._cpu_files, 'Failed to detect CPUs.'
22    self._cpu_file_list = ' '.join(self._cpu_files)
23    logging.info('CPUs found: %s', self._cpu_file_list)
24    self._have_mpdecision = self._device.FileExists('/system/bin/mpdecision')
25
26  def SetHighPerfMode(self):
27    """Sets the highest stable performance mode for the device."""
28    try:
29      self._device.EnableRoot()
30    except device_errors.CommandFailedError:
31      message = 'Need root for performance mode. Results may be NOISY!!'
32      logging.warning(message)
33      # Add an additional warning at exit, such that it's clear that any results
34      # may be different/noisy (due to the lack of intended performance mode).
35      atexit.register(logging.warning, message)
36      return
37
38    product_model = self._device.product_model
39    # TODO(epenner): Enable on all devices (http://crbug.com/383566)
40    if 'Nexus 4' == product_model:
41      self._ForceAllCpusOnline(True)
42      if not self._AllCpusAreOnline():
43        logging.warning('Failed to force CPUs online. Results may be NOISY!')
44      self._SetScalingGovernorInternal('performance')
45    elif 'Nexus 5' == product_model:
46      self._ForceAllCpusOnline(True)
47      if not self._AllCpusAreOnline():
48        logging.warning('Failed to force CPUs online. Results may be NOISY!')
49      self._SetScalingGovernorInternal('performance')
50      self._SetScalingMaxFreq(1190400)
51      self._SetMaxGpuClock(200000000)
52    else:
53      self._SetScalingGovernorInternal('performance')
54
55  def SetPerfProfilingMode(self):
56    """Enables all cores for reliable perf profiling."""
57    self._ForceAllCpusOnline(True)
58    self._SetScalingGovernorInternal('performance')
59    if not self._AllCpusAreOnline():
60      if not self._device.HasRoot():
61        raise RuntimeError('Need root to force CPUs online.')
62      raise RuntimeError('Failed to force CPUs online.')
63
64  def SetDefaultPerfMode(self):
65    """Sets the performance mode for the device to its default mode."""
66    if not self._device.HasRoot():
67      return
68    product_model = self._device.product_model
69    if 'Nexus 5' == product_model:
70      if self._AllCpusAreOnline():
71        self._SetScalingMaxFreq(2265600)
72        self._SetMaxGpuClock(450000000)
73
74    governor_mode = {
75        'GT-I9300': 'pegasusq',
76        'Galaxy Nexus': 'interactive',
77        'Nexus 4': 'ondemand',
78        'Nexus 5': 'ondemand',
79        'Nexus 7': 'interactive',
80        'Nexus 10': 'interactive'
81    }.get(product_model, 'ondemand')
82    self._SetScalingGovernorInternal(governor_mode)
83    self._ForceAllCpusOnline(False)
84
85  def GetCpuInfo(self):
86    online = (output.rstrip() == '1' and status == 0
87              for (_, output, status) in self._ForEachCpu('cat "$CPU/online"'))
88    governor = (output.rstrip() if status == 0 else None
89                for (_, output, status)
90                in self._ForEachCpu('cat "$CPU/cpufreq/scaling_governor"'))
91    return zip(self._cpu_files, online, governor)
92
93  def _ForEachCpu(self, cmd):
94    script = '; '.join([
95        'for CPU in %s' % self._cpu_file_list,
96        'do %s' % cmd,
97        'echo -n "%~%$?%~%"',
98        'done'
99    ])
100    output = self._device.RunShellCommand(
101        script, cwd=self._CPU_PATH, check_return=True, as_root=True)
102    output = '\n'.join(output).split('%~%')
103    return zip(self._cpu_files, output[0::2], (int(c) for c in output[1::2]))
104
105  def _WriteEachCpuFile(self, path, value):
106    results = self._ForEachCpu(
107        'test -e "$CPU/{path}" && echo {value} > "$CPU/{path}"'.format(
108            path=path, value=value))
109    cpus = ' '.join(cpu for (cpu, _, status) in results if status == 0)
110    if cpus:
111      logging.info('Successfully set %s to %r on: %s', path, value, cpus)
112    else:
113      logging.warning('Failed to set %s to %r on any cpus', path, value)
114
115  def _SetScalingGovernorInternal(self, value):
116    self._WriteEachCpuFile('cpufreq/scaling_governor', value)
117
118  def _SetScalingMaxFreq(self, value):
119    self._WriteEachCpuFile('cpufreq/scaling_max_freq', '%d' % value)
120
121  def _SetMaxGpuClock(self, value):
122    self._device.WriteFile('/sys/class/kgsl/kgsl-3d0/max_gpuclk',
123                           str(value),
124                           as_root=True)
125
126  def _AllCpusAreOnline(self):
127    results = self._ForEachCpu('cat "$CPU/online"')
128    # TODO(epenner): Investigate why file may be missing
129    # (http://crbug.com/397118)
130    return all(output.rstrip() == '1' and status == 0
131               for (cpu, output, status) in results
132               if cpu != 'cpu0')
133
134  def _ForceAllCpusOnline(self, force_online):
135    """Enable all CPUs on a device.
136
137    Some vendors (or only Qualcomm?) hot-plug their CPUs, which can add noise
138    to measurements:
139    - In perf, samples are only taken for the CPUs that are online when the
140      measurement is started.
141    - The scaling governor can't be set for an offline CPU and frequency scaling
142      on newly enabled CPUs adds noise to both perf and tracing measurements.
143
144    It appears Qualcomm is the only vendor that hot-plugs CPUs, and on Qualcomm
145    this is done by "mpdecision".
146
147    """
148    if self._have_mpdecision:
149      script = 'stop mpdecision' if force_online else 'start mpdecision'
150      self._device.RunShellCommand(script, check_return=True, as_root=True)
151
152    if not self._have_mpdecision and not self._AllCpusAreOnline():
153      logging.warning('Unexpected cpu hot plugging detected.')
154
155    if force_online:
156      self._ForEachCpu('echo 1 > "$CPU/online"')
157