1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import logging
18import time
19
20from vts.runners.host import asserts
21from vts.runners.host import const
22
23
24class CpuFrequencyScalingController(object):
25    """CPU Frequency Scaling Controller.
26
27    The implementation is based on the special files in
28    /sys/devices/system/cpu/. CPU availability is shown in multiple files,
29    including online, present, and possible. This class assumes that a present
30    CPU may dynamically switch its online status. If a CPU is online, its
31    frequency scaling can be adjusted by reading/writing the files in
32    cpuX/cpufreq/ where X is the CPU number.
33
34    Attributes:
35        _dut: the target device DUT instance.
36        _shell: Shell mirror object for communication with a target.
37        _min_cpu_number: integer, the min CPU number.
38        _max_cpu_number; integer, the max CPU number.
39        _theoretical_max_frequency: a dict where its key is the CPU number and
40                                    its value is an integer containing the
41                                    theoretical max CPU frequency.
42        _perf_override: boolean, true if this module has switched the device from
43                        its normal cpufreq governor to the performance
44                        governor.
45        _saved_governors: list of strings, the saved cpufreq governor for each
46                          CPU on the device.
47    """
48
49    def __init__(self, dut):
50        self._dut = dut
51        self._init = False
52
53    def Init(self):
54        """Creates a shell mirror object and reads the configuration values."""
55        if self._init:
56            return
57        self._shell = self._dut.shell
58        self._min_cpu_number, self._max_cpu_number = self._LoadMinAndMaxCpuNo()
59        self._theoretical_max_frequency = {}
60        self._perf_override = False
61        self._saved_governors = None
62        self._init = True
63
64    def _LoadMinAndMaxCpuNo(self):
65        """Reads the min and max CPU numbers from sysfs.
66
67        Returns:
68            integer: min CPU number (inclusive)
69            integer: max CPU number (exclusive)
70        """
71        results = self._shell.Execute("cat /sys/devices/system/cpu/present")
72        asserts.assertEqual(len(results[const.STDOUT]), 1)
73        stdout_lines = results[const.STDOUT][0].split("\n")
74        stdout_split = stdout_lines[0].split('-')
75        asserts.assertLess(len(stdout_split), 3)
76        low = stdout_split[0]
77        high = stdout_split[1] if len(stdout_split) == 2 else low
78        logging.debug("present cpus: %s : %s" % (low, high))
79        return int(low), int(high) + 1
80
81    def GetMinAndMaxCpuNo(self):
82        """Returns the min and max CPU numbers.
83
84        Returns:
85            integer: min CPU number (inclusive)
86            integer: max CPU number (exclusive)
87        """
88        return self._min_cpu_number, self._max_cpu_number
89
90    def _GetTheoreticalMaxFrequency(self, cpu_no):
91        """Reads max value from cpufreq/scaling_available_frequencies.
92
93        If the read operation is successful, the return value is kept in
94        _theoretical_max_frequency as a cache.
95
96        Args:
97            cpu_no: integer, the CPU number.
98
99        Returns:
100            An integer which is the max frequency read from the file.
101            None if the file cannot be read.
102        """
103        if cpu_no in self._theoretical_max_frequency:
104            return self._theoretical_max_frequency[cpu_no]
105        results = self._shell.Execute(
106            "cat /sys/devices/system/cpu/cpu%s/"
107            "cpufreq/scaling_available_frequencies" % cpu_no)
108        asserts.assertEqual(1, len(results[const.EXIT_CODE]))
109        if not results[const.EXIT_CODE][0]:
110            freq = [int(x) for x in results[const.STDOUT][0].split()]
111            self._theoretical_max_frequency[cpu_no] = max(freq)
112            return self._theoretical_max_frequency[cpu_no]
113        else:
114            logging.warn("cpufreq/scaling_available_frequencies for cpu %s"
115                         " not set.", cpu_no)
116            return None
117
118    def ChangeCpuGovernor(self, modes):
119        """Changes the CPU governor mode of all the CPUs on the device.
120
121        Args:
122            modes: list of expected CPU governor modes, e.g., 'performance'
123                   or 'schedutil'. The length of the list must be equal to
124                   the number of CPUs on the device.
125
126        Returns:
127            A list of the previous governor modes if successful, None otherwise.
128        """
129        self.Init()
130        asserts.assertEqual(self._max_cpu_number - self._min_cpu_number,
131                            len(modes))
132        # save current governor settings
133        target_cmd = []
134        prev_govs = []
135        for cpu_no in range(self._min_cpu_number, self._max_cpu_number):
136            target_cmd.append("cat /sys/devices/system/cpu/cpu%s/cpufreq/"
137                              "scaling_governor" % cpu_no)
138        results = self._shell.Execute(target_cmd)
139        asserts.assertEqual(self._max_cpu_number - self._min_cpu_number,
140                            len(results[const.STDOUT]))
141        if any(results[const.EXIT_CODE]):
142            logging.warn("Unable to save governors")
143            logging.warn("Stderr for saving scaling_governor: %s",
144                         results[const.STDERR])
145            return
146        for cpu_no in range(self._min_cpu_number, self._max_cpu_number):
147            prev_govs.append(results[const.STDOUT][cpu_no].rstrip())
148        # set new governor
149        target_cmd = []
150        for cpu_no in range(self._min_cpu_number, self._max_cpu_number):
151            target_cmd.append(
152                "echo %s > /sys/devices/system/cpu/cpu%s/cpufreq/"
153                "scaling_governor" % (modes[cpu_no], cpu_no))
154        results = self._shell.Execute(target_cmd)
155        asserts.assertEqual(self._max_cpu_number - self._min_cpu_number,
156                            len(results[const.STDOUT]))
157        if any(results[const.EXIT_CODE]):
158            logging.warn("Can't change CPU governor.")
159            logging.warn("Stderr for changing scaling_governor: %s",
160                         results[const.STDERR])
161            return
162        return prev_govs
163
164    def DisableCpuScaling(self):
165        """Disable CPU frequency scaling on the device."""
166        self.Init()
167        if self._perf_override:
168            logging.warn(
169                "DisableCpuScaling called while scaling already disabled.")
170            return
171        new_govs = []
172        for cpu_no in range(self._min_cpu_number, self._max_cpu_number):
173            new_govs.append("performance")
174        prev_govs = self.ChangeCpuGovernor(new_govs)
175        if prev_govs is not None:
176            self._saved_governors = prev_govs
177            self._perf_override = True
178
179    def EnableCpuScaling(self):
180        """Enable CPU frequency scaling on the device."""
181        self.Init()
182        if not self._perf_override:
183            logging.warn(
184                "EnableCpuScaling called while scaling already enabled.")
185            return
186        if self._saved_governors is None:
187            logging.warn(
188                "EnableCpuScaling called and _saved_governors is None.")
189            return
190        self.ChangeCpuGovernor(self._saved_governors)
191        self._perf_override = False
192
193    def IsUnderThermalThrottling(self):
194        """Checks whether a target device is under thermal throttling.
195
196        Returns:
197            True if the current CPU frequency is not the theoretical max,
198            False otherwise.
199        """
200        self.Init()
201        for cpu_no in range(self._min_cpu_number, self._max_cpu_number):
202            results = self._shell.Execute([
203                "cat /sys/devices/system/cpu/cpu%s/cpufreq/scaling_max_freq" %
204                cpu_no,
205                "cat /sys/devices/system/cpu/cpu%s/cpufreq/scaling_cur_freq" %
206                cpu_no
207            ])
208            asserts.assertEqual(2, len(results[const.STDOUT]))
209            if any(results[const.EXIT_CODE]):
210                logging.warn(
211                    "Can't check the current and/or max CPU frequency.")
212                logging.warn("Stderr for scaling_max_freq: %s",
213                             results[const.STDERR][0])
214                logging.warn("Stderr for scaling_cur_freq: %s",
215                             results[const.STDERR][1])
216                return False
217            configurable_max_frequency = results[const.STDOUT][0].strip()
218            current_frequency = results[const.STDOUT][1].strip()
219            if configurable_max_frequency > current_frequency:
220                logging.error(
221                    "CPU%s: Configurable max frequency %s > current frequency %s",
222                    cpu_no, configurable_max_frequency, current_frequency)
223                return True
224            theoretical_max_frequency = self._GetTheoreticalMaxFrequency(
225                cpu_no)
226            if (theoretical_max_frequency is not None
227                    and theoretical_max_frequency > int(current_frequency)):
228                logging.error(
229                    "CPU%s, Theoretical max frequency %d > scaling current frequency %s",
230                    cpu_no, theoretical_max_frequency, current_frequency)
231                return True
232        return False
233
234    def SkipIfThermalThrottling(self, retry_delay_secs=0):
235        """Skips the current test case if a target device is under thermal throttling.
236
237        Args:
238            retry_delay_secs: integer, if not 0, retry after the specified seconds.
239        """
240        throttling = self.IsUnderThermalThrottling()
241        if throttling and retry_delay_secs > 0:
242            logging.info("Waiting %s seconds for the target CPU to cool down.",
243                         retry_delay_secs)
244            time.sleep(retry_delay_secs)
245            throttling = self.IsUnderThermalThrottling()
246        asserts.skipIf(throttling, "Thermal throttling")
247