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