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 import decorators
11from telemetry.core.platform import power_monitor
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  def __init__(self, linux_based_platform_backend):
22    """Constructor.
23
24    Args:
25        linux_based_platform_backend: A LinuxBasedPlatformBackend object.
26
27    Attributes:
28        _browser: The browser to monitor.
29        _cpus: A list of the CPUs on the target device.
30        _end_time: The time the test stopped monitoring power.
31        _final_cstate: The c-state residency times after the test.
32        _final_freq: The CPU frequency times after the test.
33        _initial_cstate: The c-state residency times before the test.
34        _initial_freq: The CPU frequency times before the test.
35        _platform: A LinuxBasedPlatformBackend object associated with the
36            target platform.
37        _start_time: The time the test started monitoring power.
38    """
39    super(SysfsPowerMonitor, self).__init__()
40    self._browser = None
41    self._cpus = None
42    self._final_cstate = None
43    self._final_freq = None
44    self._initial_cstate = None
45    self._initial_freq = None
46    self._platform = linux_based_platform_backend
47
48  @decorators.Cache
49  def CanMonitorPower(self):
50    return bool(self._platform.RunCommand(
51        'if [ -e %s ]; then echo true; fi' % CPU_PATH))
52
53  def StartMonitoringPower(self, browser):
54    assert not self._browser, 'Must call StopMonitoringPower().'
55    self._browser = browser
56    if self.CanMonitorPower():
57      self._cpus = filter(
58          lambda x: re.match(r'^cpu[0-9]+', x),
59          self._platform.RunCommand('ls %s' % CPU_PATH).split())
60      self._initial_freq = self.GetCpuFreq()
61      self._initial_cstate = self.GetCpuState()
62
63  def StopMonitoringPower(self):
64    assert self._browser, 'StartMonitoringPower() not called.'
65    try:
66      out = {}
67      if SysfsPowerMonitor.CanMonitorPower(self):
68        self._final_freq = self.GetCpuFreq()
69        self._final_cstate = self.GetCpuState()
70        frequencies = SysfsPowerMonitor.ComputeCpuStats(
71            SysfsPowerMonitor.ParseFreqSample(self._initial_freq),
72            SysfsPowerMonitor.ParseFreqSample(self._final_freq))
73        cstates = SysfsPowerMonitor.ComputeCpuStats(
74            self._platform.ParseCStateSample(self._initial_cstate),
75            self._platform.ParseCStateSample(self._final_cstate))
76        for cpu in frequencies:
77          out[cpu] = {'frequency_percent': frequencies[cpu]}
78          out[cpu]['cstate_residency_percent'] = cstates[cpu]
79      return out
80    finally:
81      self._browser = None
82
83  def GetCpuState(self):
84    """Retrieve CPU c-state residency times from the device.
85
86    Returns:
87        Dictionary containing c-state residency times for each CPU.
88    """
89    stats = {}
90    for cpu in self._cpus:
91      cpu_state_path = os.path.join(CPU_PATH, cpu, 'cpuidle/state*')
92      stats[cpu] = self._platform.RunCommand(
93          'cat %s %s %s; date +%%s' % (os.path.join(cpu_state_path, 'name'),
94          os.path.join(cpu_state_path, 'time'),
95          os.path.join(cpu_state_path, 'latency')))
96    return stats
97
98  def GetCpuFreq(self):
99    """Retrieve CPU frequency times from the device.
100
101    Returns:
102        Dictionary containing frequency times for each CPU.
103    """
104    stats = {}
105    for cpu in self._cpus:
106      cpu_freq_path = os.path.join(
107          CPU_PATH, cpu, 'cpufreq/stats/time_in_state')
108      try:
109        stats[cpu] = self._platform.GetFileContents(cpu_freq_path)
110      except Exception as e:
111        logging.warning(
112            'Cannot read cpu frequency times in %s due to error: %s' %
113            (cpu_freq_path, e.message))
114        stats[cpu] = None
115    return stats
116
117  @staticmethod
118  def ParseFreqSample(sample):
119    """Parse a single frequency sample.
120
121    Args:
122        sample: The single sample of frequency data to be parsed.
123
124    Returns:
125        A dictionary associating a frequency with a time.
126    """
127    sample_stats = {}
128    for cpu in sample:
129      frequencies = {}
130      if sample[cpu] is None:
131        sample_stats[cpu] = None
132        continue
133      for line in sample[cpu].splitlines():
134        pair = line.split()
135        freq = int(pair[0]) * 10 ** 3
136        timeunits = int(pair[1])
137        if freq in frequencies:
138          frequencies[freq] += timeunits
139        else:
140          frequencies[freq] = timeunits
141      sample_stats[cpu] = frequencies
142    return sample_stats
143
144  @staticmethod
145  def ComputeCpuStats(initial, final):
146    """Parse the CPU c-state and frequency values saved during monitoring.
147
148    Args:
149        initial: The parsed dictionary of initial statistics to be converted
150        into percentages.
151        final: The parsed dictionary of final statistics to be converted
152        into percentages.
153
154    Returns:
155        Dictionary containing percentages for each CPU as well as an average
156        across all CPUs.
157    """
158    cpu_stats = {}
159    # Each core might have different states or frequencies, so keep track of
160    # the total time in a state or frequency and how many cores report a time.
161    cumulative_times = collections.defaultdict(lambda: (0, 0))
162    for cpu in initial:
163      current_cpu = {}
164      total = 0
165      if not initial[cpu] or not final[cpu]:
166        cpu_stats[cpu] = collections.defaultdict(int)
167        continue
168      for state in initial[cpu]:
169        current_cpu[state] = final[cpu][state] - initial[cpu][state]
170        total += current_cpu[state]
171      for state in current_cpu:
172        current_cpu[state] /= (float(total) / 100.0)
173        # Calculate the average c-state residency across all CPUs.
174        time, count = cumulative_times[state]
175        cumulative_times[state] = (time + current_cpu[state], count + 1)
176      cpu_stats[cpu] = current_cpu
177    average = {}
178    for state in cumulative_times:
179      time, count = cumulative_times[state]
180      average[state] = time / float(count)
181    cpu_stats['whole_package'] = average
182    return cpu_stats
183
184  @staticmethod
185  def CombineResults(cpu_stats, power_stats):
186    """Add frequency and c-state residency data to the power data.
187
188    Args:
189        cpu_stats: Dictionary containing CPU statistics.
190        power_stats: Dictionary containing power statistics.
191
192    Returns:
193        Dictionary in the format returned by StopMonitoringPower.
194    """
195    if not cpu_stats:
196      return power_stats
197    if 'component_utilization' not in power_stats:
198      power_stats['component_utilization'] = {}
199    for cpu in cpu_stats:
200      power_stats['component_utilization'][cpu] = cpu_stats[cpu]
201    return power_stats
202