1# Copyright 2013 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
5"""Profiler using data collected from a Monsoon power meter.
6
7http://msoon.com/LabEquipment/PowerMonitor/
8Data collected is a namedtuple of (amps, volts), at 5000 samples/second.
9Output graph plots power in watts over time in seconds.
10"""
11
12import csv
13import multiprocessing
14
15from telemetry.core import exceptions
16from telemetry.core.platform import profiler
17from telemetry.core.platform.profiler import monsoon
18from telemetry.util import statistics
19
20
21def _CollectData(output_path, is_collecting):
22  mon = monsoon.Monsoon(wait=False)
23  # Note: Telemetry requires the device to be connected by USB, but that
24  # puts it in charging mode. This increases the power consumption.
25  mon.SetUsbPassthrough(1)
26  # Nominal Li-ion voltage is 3.7V, but it puts out 4.2V at max capacity. Use
27  # 4.0V to simulate a "~80%" charged battery. Google "li-ion voltage curve".
28  # This is true only for a single cell. (Most smartphones, some tablets.)
29  mon.SetVoltage(4.0)
30
31  samples = []
32  try:
33    mon.StartDataCollection()
34    # Do one CollectData() to make the Monsoon set up, which takes about
35    # 0.3 seconds, and only signal that we've started after that.
36    mon.CollectData()
37    is_collecting.set()
38    while is_collecting.is_set():
39      samples += mon.CollectData()
40  finally:
41    mon.StopDataCollection()
42
43  # Add x-axis labels.
44  plot_data = [(i / 5000., sample.amps * sample.volts)
45               for i, sample in enumerate(samples)]
46
47  # Print data in csv.
48  with open(output_path, 'w') as output_file:
49    output_writer = csv.writer(output_file)
50    output_writer.writerows(plot_data)
51    output_file.flush()
52
53  power_samples = [s.amps * s.volts for s in samples]
54
55  print 'Monsoon profile power readings in watts:'
56  print ('  Total    = %f' % statistics.TrapezoidalRule(power_samples, 1/5000.))
57  print ('  Average  = %f' % statistics.ArithmeticMean(power_samples) +
58         '+-%f' % statistics.StandardDeviation(power_samples))
59  print ('  Peak     = %f' % max(power_samples))
60  print ('  Duration = %f' % (len(power_samples) / 5000.))
61
62  print 'To view the Monsoon profile, run:'
63  print ('  echo "set datafile separator \',\'; plot \'%s\' with lines" | '
64      'gnuplot --persist' % output_path)
65
66
67class MonsoonProfiler(profiler.Profiler):
68  def __init__(self, browser_backend, platform_backend, output_path, state):
69    super(MonsoonProfiler, self).__init__(
70        browser_backend, platform_backend, output_path, state)
71    # We collect the data in a separate process, so we can continuously
72    # read the samples from the USB port while running the test.
73    self._is_collecting = multiprocessing.Event()
74    self._collector = multiprocessing.Process(
75        target=_CollectData, args=(output_path, self._is_collecting))
76    self._collector.start()
77    if not self._is_collecting.wait(timeout=0.5):
78      self._collector.terminate()
79      raise exceptions.ProfilingException('Failed to start data collection.')
80
81  @classmethod
82  def name(cls):
83    return 'monsoon'
84
85  @classmethod
86  def is_supported(cls, browser_type):
87    try:
88      monsoon.Monsoon(wait=False)
89    except EnvironmentError:
90      return False
91    else:
92      return True
93
94  def CollectProfile(self):
95    self._is_collecting.clear()
96    self._collector.join()
97    return [self._output_path]
98