1# Copyright 2014 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 collections 6import logging 7import os 8import re 9 10from telemetry.internal.platform import power_monitor 11from telemetry import decorators 12 13 14CPU_PATH = '/sys/devices/system/cpu/' 15 16 17class SysfsPowerMonitor(power_monitor.PowerMonitor): 18 """PowerMonitor that relies on sysfs to monitor CPU statistics on several 19 different platforms. 20 """ 21 # TODO(rnephew): crbug.com/513453 22 # Convert all platforms to use standalone power monitors. 23 def __init__(self, linux_based_platform_backend, standalone=False): 24 """Constructor. 25 26 Args: 27 linux_based_platform_backend: A LinuxBasedPlatformBackend object. 28 standalone: If it is not wrapping another monitor, set to True. 29 30 Attributes: 31 _cpus: A list of the CPUs on the target device. 32 _end_time: The time the test stopped monitoring power. 33 _final_cstate: The c-state residency times after the test. 34 _final_freq: The CPU frequency times after the test. 35 _initial_cstate: The c-state residency times before the test. 36 _initial_freq: The CPU frequency times before the test. 37 _platform: A LinuxBasedPlatformBackend object associated with the 38 target platform. 39 _start_time: The time the test started monitoring power. 40 """ 41 super(SysfsPowerMonitor, self).__init__() 42 self._cpus = None 43 self._final_cstate = None 44 self._final_freq = None 45 self._initial_cstate = None 46 self._initial_freq = None 47 self._platform = linux_based_platform_backend 48 self._standalone = standalone 49 50 @decorators.Cache 51 def CanMonitorPower(self): 52 return bool(self._platform.RunCommand( 53 'if [ -e %s ]; then echo true; fi' % CPU_PATH)) 54 55 def StartMonitoringPower(self, browser): 56 del browser # unused 57 self._CheckStart() 58 if self.CanMonitorPower(): 59 self._cpus = filter( # pylint: disable=deprecated-lambda 60 lambda x: re.match(r'^cpu[0-9]+', x), 61 self._platform.RunCommand('ls %s' % CPU_PATH).split()) 62 self._initial_freq = self.GetCpuFreq() 63 self._initial_cstate = self.GetCpuState() 64 65 def StopMonitoringPower(self): 66 self._CheckStop() 67 try: 68 out = {} 69 if SysfsPowerMonitor.CanMonitorPower(self): 70 self._final_freq = self.GetCpuFreq() 71 self._final_cstate = self.GetCpuState() 72 frequencies = SysfsPowerMonitor.ComputeCpuStats( 73 SysfsPowerMonitor.ParseFreqSample(self._initial_freq), 74 SysfsPowerMonitor.ParseFreqSample(self._final_freq)) 75 cstates = SysfsPowerMonitor.ComputeCpuStats( 76 self._platform.ParseCStateSample(self._initial_cstate), 77 self._platform.ParseCStateSample(self._final_cstate)) 78 for cpu in frequencies: 79 out[cpu] = {'frequency_percent': frequencies.get(cpu)} 80 out[cpu] = {'cstate_residency_percent': cstates.get(cpu)} 81 if self._standalone: 82 return self.CombineResults(out, {}) 83 return out 84 finally: 85 self._initial_cstate = None 86 self._initial_freq = None 87 88 def GetCpuState(self): 89 """Retrieve CPU c-state residency times from the device. 90 91 Returns: 92 Dictionary containing c-state residency times for each CPU. 93 """ 94 stats = {} 95 for cpu in self._cpus: 96 cpu_idle_path = os.path.join(CPU_PATH, cpu, 'cpuidle') 97 if not self._platform.PathExists(cpu_idle_path): 98 logging.warning( 99 'Cannot read cpu c-state residency times for %s due to %s not exist' 100 % (cpu, cpu_idle_path)) 101 continue 102 cpu_state_path = os.path.join(cpu_idle_path, 'state*') 103 output = self._platform.RunCommand( 104 'cat %s %s %s; date +%%s' % ( 105 os.path.join(cpu_state_path, 'name'), 106 os.path.join(cpu_state_path, 'time'), 107 os.path.join(cpu_state_path, 'latency'))) 108 stats[cpu] = re.sub('\n\n+', '\n', output) 109 return stats 110 111 def GetCpuFreq(self): 112 """Retrieve CPU frequency times from the device. 113 114 Returns: 115 Dictionary containing frequency times for each CPU. 116 """ 117 stats = {} 118 for cpu in self._cpus: 119 cpu_freq_path = os.path.join( 120 CPU_PATH, cpu, 'cpufreq/stats/time_in_state') 121 if not self._platform.PathExists(cpu_freq_path): 122 logging.warning( 123 'Cannot read cpu frequency times for %s due to %s not existing' 124 % (cpu, cpu_freq_path)) 125 stats[cpu] = None 126 continue 127 try: 128 stats[cpu] = self._platform.GetFileContents(cpu_freq_path) 129 except Exception as e: 130 logging.warning( 131 'Cannot read cpu frequency times in %s due to error: %s' % 132 (cpu_freq_path, e.message)) 133 stats[cpu] = None 134 return stats 135 136 @staticmethod 137 def ParseFreqSample(sample): 138 """Parse a single frequency sample. 139 140 Args: 141 sample: The single sample of frequency data to be parsed. 142 143 Returns: 144 A dictionary associating a frequency with a time. 145 """ 146 sample_stats = {} 147 for cpu in sample: 148 frequencies = {} 149 if sample[cpu] is None: 150 sample_stats[cpu] = None 151 continue 152 for line in sample[cpu].splitlines(): 153 pair = line.split() 154 freq = int(pair[0]) * 10 ** 3 155 timeunits = int(pair[1]) 156 if freq in frequencies: 157 frequencies[freq] += timeunits 158 else: 159 frequencies[freq] = timeunits 160 sample_stats[cpu] = frequencies 161 return sample_stats 162 163 @staticmethod 164 def ComputeCpuStats(initial, final): 165 """Parse the CPU c-state and frequency values saved during monitoring. 166 167 Args: 168 initial: The parsed dictionary of initial statistics to be converted 169 into percentages. 170 final: The parsed dictionary of final statistics to be converted 171 into percentages. 172 173 Returns: 174 Dictionary containing percentages for each CPU as well as an average 175 across all CPUs. 176 """ 177 cpu_stats = {} 178 # Each core might have different states or frequencies, so keep track of 179 # the total time in a state or frequency and how many cores report a time. 180 cumulative_times = collections.defaultdict(lambda: (0, 0)) 181 for cpu in initial: 182 current_cpu = {} 183 total = 0 184 if not initial[cpu] or not final[cpu]: 185 cpu_stats[cpu] = collections.defaultdict(int) 186 continue 187 for state in initial[cpu]: 188 current_cpu[state] = final[cpu][state] - initial[cpu][state] 189 total += current_cpu[state] 190 if total == 0: 191 # Somehow it's possible for initial and final to have the same sum, 192 # but a different distribution, making total == 0. crbug.com/426430 193 cpu_stats[cpu] = collections.defaultdict(int) 194 continue 195 for state in current_cpu: 196 current_cpu[state] /= (float(total) / 100.0) 197 # Calculate the average c-state residency across all CPUs. 198 time, count = cumulative_times[state] 199 cumulative_times[state] = (time + current_cpu[state], count + 1) 200 cpu_stats[cpu] = current_cpu 201 average = {} 202 for state in cumulative_times: 203 time, count = cumulative_times[state] 204 average[state] = time / float(count) 205 cpu_stats['platform_info'] = average 206 return cpu_stats 207 208 @staticmethod 209 def CombineResults(cpu_stats, power_stats): 210 """Add frequency and c-state residency data to the power data. 211 212 Args: 213 cpu_stats: Dictionary containing CPU statistics. 214 power_stats: Dictionary containing power statistics. 215 216 Returns: 217 Dictionary in the format returned by StopMonitoringPower. 218 """ 219 if not cpu_stats: 220 return power_stats 221 if 'component_utilization' not in power_stats: 222 power_stats['component_utilization'] = {} 223 if 'platform_info' in cpu_stats: 224 if 'platform_info' not in power_stats: 225 power_stats['platform_info'] = {} 226 power_stats['platform_info'].update(cpu_stats['platform_info']) 227 del cpu_stats['platform_info'] 228 for cpu in cpu_stats: 229 power_stats['component_utilization'][cpu] = cpu_stats[cpu] 230 return power_stats 231