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 re
8
9from telemetry import decorators
10from telemetry.internal.platform.power_monitor import sysfs_power_monitor
11
12
13class CrosPowerMonitor(sysfs_power_monitor.SysfsPowerMonitor):
14  """PowerMonitor that relies on 'dump_power_status' to monitor power
15  consumption of a single ChromeOS application.
16  """
17  def __init__(self, platform_backend):
18    """Constructor.
19
20    Args:
21        platform_backend: A LinuxBasedPlatformBackend object.
22
23    Attributes:
24        _initial_power: The result of 'dump_power_status' before the test.
25        _start_time: The epoch time at which the test starts executing.
26    """
27    super(CrosPowerMonitor, self).__init__(platform_backend)
28    self._initial_power = None
29    self._start_time = None
30
31  @decorators.Cache
32  def CanMonitorPower(self):
33    return super(CrosPowerMonitor, self).CanMonitorPower()
34
35  def StartMonitoringPower(self, browser):
36    super(CrosPowerMonitor, self).StartMonitoringPower(browser)
37    if self._IsOnBatteryPower():
38      sample = self._platform.RunCommand(['dump_power_status;', 'date', '+%s'])
39      self._initial_power, self._start_time = CrosPowerMonitor.SplitSample(
40          sample)
41    else:
42      logging.warning('Device not on battery power during power monitoring. '
43                      'Results may be incorrect.')
44
45  def StopMonitoringPower(self):
46    # Don't need to call self._CheckStop here; it's called by the superclass
47    cpu_stats = super(CrosPowerMonitor, self).StopMonitoringPower()
48    power_stats = {}
49    if self._IsOnBatteryPower():
50      sample = self._platform.RunCommand(['dump_power_status;', 'date', '+%s'])
51      final_power, end_time = CrosPowerMonitor.SplitSample(sample)
52      # The length of the test is used to measure energy consumption.
53      length_h = (end_time - self._start_time) / 3600.0
54      power_stats = CrosPowerMonitor.ParsePower(self._initial_power,
55                                                final_power, length_h)
56    else:
57      logging.warning('Device not on battery power during power monitoring. '
58                      'Results may be incorrect.')
59    return CrosPowerMonitor.CombineResults(cpu_stats, power_stats)
60
61  @staticmethod
62  def SplitSample(sample):
63    """Splits a power and time sample into the two separate values.
64
65    Args:
66        sample: The result of calling 'dump_power_status; date +%s' on the
67            device.
68
69    Returns:
70        A tuple of power sample and epoch time of the sample.
71    """
72    sample = sample.strip()
73    index = sample.rfind('\n')
74    power = sample[:index]
75    time = sample[index + 1:]
76    return power, int(time)
77
78  @staticmethod
79  def IsOnBatteryPower(status, board):
80    """Determines if the devices is being charged.
81
82    Args:
83        status: The parsed result of 'dump_power_status'
84        board: The name of the board running the test.
85
86    Returns:
87        True if the device is on battery power; False otherwise.
88    """
89    on_battery = status['line_power_connected'] == '0'
90    # Butterfly can incorrectly report AC online for some time after unplug.
91    # Check battery discharge state to confirm.
92    if board == 'butterfly':
93      on_battery |= status['battery_discharging'] == '1'
94    return on_battery
95
96  def _IsOnBatteryPower(self):
97    """Determines if the device is being charged.
98
99    Returns:
100        True if the device is on battery power; False otherwise.
101    """
102    status = CrosPowerMonitor.ParsePowerStatus(
103        self._platform.RunCommand(['dump_power_status']))
104    board_data = self._platform.RunCommand(['cat', '/etc/lsb-release'])
105    board = re.search('BOARD=(.*)', board_data).group(1)
106    return CrosPowerMonitor.IsOnBatteryPower(status, board)
107
108  @staticmethod
109  def ParsePowerStatus(sample):
110    """Parses 'dump_power_status' command output.
111
112    Args:
113        sample: The output of 'dump_power_status'
114
115    Returns:
116        Dictionary containing all fields from 'dump_power_status'
117    """
118    rv = collections.defaultdict(dict)
119    for ln in sample.splitlines():
120      words = ln.split()
121      assert len(words) == 2
122      rv[words[0]] = words[1]
123    return dict(rv)
124
125  @staticmethod
126  def ParsePower(initial_stats, final_stats, length_h):
127    """Parse output of 'dump_power_status'
128
129    Args:
130        initial_stats: The output of 'dump_power_status' before the test.
131        final_stats: The output of 'dump_power_status' after the test.
132        length_h: The length of the test in hours.
133
134    Returns:
135        Dictionary in the format returned by StopMonitoringPower().
136    """
137    initial = CrosPowerMonitor.ParsePowerStatus(initial_stats)
138    final = CrosPowerMonitor.ParsePowerStatus(final_stats)
139    # The charge value reported by 'dump_power_status' is not precise enough to
140    # give meaningful results across shorter tests, so average energy rate and
141    # the length of the test are used.
142    initial_power_mw = float(initial['battery_energy_rate']) * 10 ** 3
143    final_power_mw = float(final['battery_energy_rate']) * 10 ** 3
144    average_power_mw = (initial_power_mw + final_power_mw) / 2.0
145
146    # Duplicating CrOS battery fields where applicable.
147    def CopyFinalState(field, key):
148      """Copy fields from battery final state."""
149      if field in final:
150        battery[key] = float(final[field])
151
152    battery = {}
153    CopyFinalState('battery_charge_full', 'charge_full')
154    CopyFinalState('battery_charge_full_design', 'charge_full_design')
155    CopyFinalState('battery_charge', 'charge_now')
156    CopyFinalState('battery_current', 'current_now')
157    CopyFinalState('battery_energy', 'energy')
158    CopyFinalState('battery_energy_rate', 'energy_rate')
159    CopyFinalState('battery_voltage', 'voltage_now')
160
161    return {'identifier': 'dump_power_status',
162            'power_samples_mw': [initial_power_mw, final_power_mw],
163            'energy_consumption_mwh': average_power_mw * length_h,
164            'component_utilization': {'battery': battery}}
165