| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import logging |
| import os |
| import re |
| |
| from telemetry.internal.platform import power_monitor |
| from telemetry import decorators |
| |
| |
| CPU_PATH = '/sys/devices/system/cpu/' |
| |
| |
| class SysfsPowerMonitor(power_monitor.PowerMonitor): |
| """PowerMonitor that relies on sysfs to monitor CPU statistics on several |
| different platforms. |
| """ |
| # TODO(rnephew): crbug.com/513453 |
| # Convert all platforms to use standalone power monitors. |
| def __init__(self, linux_based_platform_backend, standalone=False): |
| """Constructor. |
| |
| Args: |
| linux_based_platform_backend: A LinuxBasedPlatformBackend object. |
| standalone: If it is not wrapping another monitor, set to True. |
| |
| Attributes: |
| _cpus: A list of the CPUs on the target device. |
| _end_time: The time the test stopped monitoring power. |
| _final_cstate: The c-state residency times after the test. |
| _final_freq: The CPU frequency times after the test. |
| _initial_cstate: The c-state residency times before the test. |
| _initial_freq: The CPU frequency times before the test. |
| _platform: A LinuxBasedPlatformBackend object associated with the |
| target platform. |
| _start_time: The time the test started monitoring power. |
| """ |
| super(SysfsPowerMonitor, self).__init__() |
| self._cpus = None |
| self._final_cstate = None |
| self._final_freq = None |
| self._initial_cstate = None |
| self._initial_freq = None |
| self._platform = linux_based_platform_backend |
| self._standalone = standalone |
| |
| @decorators.Cache |
| def CanMonitorPower(self): |
| return bool(self._platform.RunCommand( |
| 'if [ -e %s ]; then echo true; fi' % CPU_PATH)) |
| |
| def StartMonitoringPower(self, browser): |
| del browser # unused |
| self._CheckStart() |
| if self.CanMonitorPower(): |
| self._cpus = filter( # pylint: disable=deprecated-lambda |
| lambda x: re.match(r'^cpu[0-9]+', x), |
| self._platform.RunCommand('ls %s' % CPU_PATH).split()) |
| self._initial_freq = self.GetCpuFreq() |
| self._initial_cstate = self.GetCpuState() |
| |
| def StopMonitoringPower(self): |
| self._CheckStop() |
| try: |
| out = {} |
| if SysfsPowerMonitor.CanMonitorPower(self): |
| self._final_freq = self.GetCpuFreq() |
| self._final_cstate = self.GetCpuState() |
| frequencies = SysfsPowerMonitor.ComputeCpuStats( |
| SysfsPowerMonitor.ParseFreqSample(self._initial_freq), |
| SysfsPowerMonitor.ParseFreqSample(self._final_freq)) |
| cstates = SysfsPowerMonitor.ComputeCpuStats( |
| self._platform.ParseCStateSample(self._initial_cstate), |
| self._platform.ParseCStateSample(self._final_cstate)) |
| for cpu in frequencies: |
| out[cpu] = {'frequency_percent': frequencies.get(cpu)} |
| out[cpu] = {'cstate_residency_percent': cstates.get(cpu)} |
| if self._standalone: |
| return self.CombineResults(out, {}) |
| return out |
| finally: |
| self._initial_cstate = None |
| self._initial_freq = None |
| |
| def GetCpuState(self): |
| """Retrieve CPU c-state residency times from the device. |
| |
| Returns: |
| Dictionary containing c-state residency times for each CPU. |
| """ |
| stats = {} |
| for cpu in self._cpus: |
| cpu_idle_path = os.path.join(CPU_PATH, cpu, 'cpuidle') |
| if not self._platform.PathExists(cpu_idle_path): |
| logging.warning( |
| 'Cannot read cpu c-state residency times for %s due to %s not exist' |
| % (cpu, cpu_idle_path)) |
| continue |
| cpu_state_path = os.path.join(cpu_idle_path, 'state*') |
| output = self._platform.RunCommand( |
| 'cat %s %s %s; date +%%s' % ( |
| os.path.join(cpu_state_path, 'name'), |
| os.path.join(cpu_state_path, 'time'), |
| os.path.join(cpu_state_path, 'latency'))) |
| stats[cpu] = re.sub('\n\n+', '\n', output) |
| return stats |
| |
| def GetCpuFreq(self): |
| """Retrieve CPU frequency times from the device. |
| |
| Returns: |
| Dictionary containing frequency times for each CPU. |
| """ |
| stats = {} |
| for cpu in self._cpus: |
| cpu_freq_path = os.path.join( |
| CPU_PATH, cpu, 'cpufreq/stats/time_in_state') |
| if not self._platform.PathExists(cpu_freq_path): |
| logging.warning( |
| 'Cannot read cpu frequency times for %s due to %s not existing' |
| % (cpu, cpu_freq_path)) |
| stats[cpu] = None |
| continue |
| try: |
| stats[cpu] = self._platform.GetFileContents(cpu_freq_path) |
| except Exception as e: |
| logging.warning( |
| 'Cannot read cpu frequency times in %s due to error: %s' % |
| (cpu_freq_path, e.message)) |
| stats[cpu] = None |
| return stats |
| |
| @staticmethod |
| def ParseFreqSample(sample): |
| """Parse a single frequency sample. |
| |
| Args: |
| sample: The single sample of frequency data to be parsed. |
| |
| Returns: |
| A dictionary associating a frequency with a time. |
| """ |
| sample_stats = {} |
| for cpu in sample: |
| frequencies = {} |
| if sample[cpu] is None: |
| sample_stats[cpu] = None |
| continue |
| for line in sample[cpu].splitlines(): |
| pair = line.split() |
| freq = int(pair[0]) * 10 ** 3 |
| timeunits = int(pair[1]) |
| if freq in frequencies: |
| frequencies[freq] += timeunits |
| else: |
| frequencies[freq] = timeunits |
| sample_stats[cpu] = frequencies |
| return sample_stats |
| |
| @staticmethod |
| def ComputeCpuStats(initial, final): |
| """Parse the CPU c-state and frequency values saved during monitoring. |
| |
| Args: |
| initial: The parsed dictionary of initial statistics to be converted |
| into percentages. |
| final: The parsed dictionary of final statistics to be converted |
| into percentages. |
| |
| Returns: |
| Dictionary containing percentages for each CPU as well as an average |
| across all CPUs. |
| """ |
| cpu_stats = {} |
| # Each core might have different states or frequencies, so keep track of |
| # the total time in a state or frequency and how many cores report a time. |
| cumulative_times = collections.defaultdict(lambda: (0, 0)) |
| for cpu in initial: |
| current_cpu = {} |
| total = 0 |
| if not initial[cpu] or not final[cpu]: |
| cpu_stats[cpu] = collections.defaultdict(int) |
| continue |
| for state in initial[cpu]: |
| current_cpu[state] = final[cpu][state] - initial[cpu][state] |
| total += current_cpu[state] |
| if total == 0: |
| # Somehow it's possible for initial and final to have the same sum, |
| # but a different distribution, making total == 0. crbug.com/426430 |
| cpu_stats[cpu] = collections.defaultdict(int) |
| continue |
| for state in current_cpu: |
| current_cpu[state] /= (float(total) / 100.0) |
| # Calculate the average c-state residency across all CPUs. |
| time, count = cumulative_times[state] |
| cumulative_times[state] = (time + current_cpu[state], count + 1) |
| cpu_stats[cpu] = current_cpu |
| average = {} |
| for state in cumulative_times: |
| time, count = cumulative_times[state] |
| average[state] = time / float(count) |
| cpu_stats['platform_info'] = average |
| return cpu_stats |
| |
| @staticmethod |
| def CombineResults(cpu_stats, power_stats): |
| """Add frequency and c-state residency data to the power data. |
| |
| Args: |
| cpu_stats: Dictionary containing CPU statistics. |
| power_stats: Dictionary containing power statistics. |
| |
| Returns: |
| Dictionary in the format returned by StopMonitoringPower. |
| """ |
| if not cpu_stats: |
| return power_stats |
| if 'component_utilization' not in power_stats: |
| power_stats['component_utilization'] = {} |
| if 'platform_info' in cpu_stats: |
| if 'platform_info' not in power_stats: |
| power_stats['platform_info'] = {} |
| power_stats['platform_info'].update(cpu_stats['platform_info']) |
| del cpu_stats['platform_info'] |
| for cpu in cpu_stats: |
| power_stats['component_utilization'][cpu] = cpu_stats[cpu] |
| return power_stats |