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