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