aboutsummaryrefslogtreecommitdiff
path: root/catapult/telemetry/telemetry/internal/platform/power_monitor/sysfs_power_monitor.py
blob: 85e5d30b681341c31ecaa69572280152cdf29109 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# 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