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 json
6import multiprocessing
7import tempfile
8import time
9
10from telemetry.core import exceptions
11from telemetry.core.platform.power_monitor import sysfs_power_monitor
12from telemetry.core.platform.profiler import monsoon
13
14
15def _MonitorPower(device, is_collecting, output):
16  """Monitoring process
17     Args:
18       device: A profiler.monsoon object to collect samples from.
19       is_collecting: the event to synchronize on.
20       output: opened file to write the samples.
21  """
22  with output:
23    samples = []
24    start_time = None
25    end_time = None
26    try:
27      device.StartDataCollection()
28      is_collecting.set()
29      # First sample also calibrate the computation.
30      device.CollectData()
31      start_time = time.time()
32      while is_collecting.is_set():
33        new_data = device.CollectData()
34        assert new_data, 'Unable to collect data from device'
35        samples += new_data
36      end_time = time.time()
37    finally:
38      device.StopDataCollection()
39    result = {
40      'duration_s': end_time - start_time,
41      'samples': samples
42    }
43    json.dump(result, output)
44
45class MonsoonPowerMonitor(sysfs_power_monitor.SysfsPowerMonitor):
46  def __init__(self, _, platform_backend):
47    super(MonsoonPowerMonitor, self).__init__(platform_backend)
48    self._powermonitor_process = None
49    self._powermonitor_output_file = None
50    self._is_collecting = None
51    self._monsoon = None
52    try:
53      self._monsoon = monsoon.Monsoon(wait=False)
54      # Nominal Li-ion voltage is 3.7V, but it puts out 4.2V at max capacity.
55      # Use 4.0V to simulate a "~80%" charged battery. Google "li-ion voltage
56      # curve". This is true only for a single cell. (Most smartphones, some
57      # tablets.)
58      self._monsoon.SetVoltage(4.0)
59    except EnvironmentError:
60      self._monsoon = None
61
62  def CanMonitorPower(self):
63    return self._monsoon is not None
64
65  def StartMonitoringPower(self, browser):
66    assert not self._powermonitor_process, (
67        'Must call StopMonitoringPower().')
68    super(MonsoonPowerMonitor, self).StartMonitoringPower(browser)
69    self._powermonitor_output_file = tempfile.TemporaryFile()
70    self._is_collecting = multiprocessing.Event()
71    self._powermonitor_process = multiprocessing.Process(
72        target=_MonitorPower,
73        args=(self._monsoon,
74              self._is_collecting,
75              self._powermonitor_output_file))
76    # Ensure child is not left behind: parent kills daemonic children on exit.
77    self._powermonitor_process.daemon = True
78    self._powermonitor_process.start()
79    if not self._is_collecting.wait(timeout=0.5):
80      self._powermonitor_process.terminate()
81      raise exceptions.ProfilingException('Failed to start data collection.')
82
83  def StopMonitoringPower(self):
84    assert self._powermonitor_process, (
85        'StartMonitoringPower() not called.')
86    try:
87      cpu_stats = super(MonsoonPowerMonitor, self).StopMonitoringPower()
88      # Tell powermonitor to take an immediate sample and join.
89      self._is_collecting.clear()
90      self._powermonitor_process.join()
91      with self._powermonitor_output_file:
92        self._powermonitor_output_file.seek(0)
93        powermonitor_output = self._powermonitor_output_file.read()
94      assert powermonitor_output, 'PowerMonitor produced no output'
95      power_stats = MonsoonPowerMonitor.ParseSamplingOutput(powermonitor_output)
96      return super(MonsoonPowerMonitor, self).CombineResults(cpu_stats,
97                                                             power_stats)
98    finally:
99      self._powermonitor_output_file = None
100      self._powermonitor_process = None
101      self._is_collecting = None
102
103  @staticmethod
104  def ParseSamplingOutput(powermonitor_output):
105    """Parse the output of of the samples collector process.
106
107    Returns:
108        Dictionary in the format returned by StopMonitoringPower().
109    """
110    power_samples = []
111    total_energy_consumption_mwh = 0
112
113    result = json.loads(powermonitor_output)
114    if result['samples']:
115      timedelta_h = result['duration_s'] / len(result['samples']) / 3600
116      for (current_a, voltage_v) in result['samples']:
117        energy_consumption_mw = current_a * voltage_v * 10**3
118        total_energy_consumption_mwh += energy_consumption_mw * timedelta_h
119        power_samples.append(energy_consumption_mw)
120
121    out_dict = {}
122    out_dict['identifier'] = 'monsoon'
123    out_dict['power_samples_mw'] = power_samples
124    out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
125
126    return out_dict
127