summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorqsr@chromium.org <qsr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-02-07 21:24:19 +0000
committerqsr@chromium.org <qsr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-02-07 21:24:19 +0000
commitbcc75b8a94f273590d12ec930d6afb7fc3b66036 (patch)
treefab94b0c658db9c2b912ff4808fda02e3f83e382
parent4259ce953309b6454b572af71626e1bb5a82934d (diff)
downloadchromium_src-bcc75b8a94f273590d12ec930d6afb7fc3b66036.zip
chromium_src-bcc75b8a94f273590d12ec930d6afb7fc3b66036.tar.gz
chromium_src-bcc75b8a94f273590d12ec930d6afb7fc3b66036.tar.bz2
[Telemetry] Initial power monitoring integration on Android
This implementation only target the N10, as this is the only device know to have a power gauge. Moreover, the result are only usable if the measurement is done while the device is not charging. R=jeremy@chromium.org,tonyg@chromium.org,bulach@chromium.org BUG=314481 Review URL: https://codereview.chromium.org/130843007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@249784 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--tools/telemetry/telemetry/core/platform/android_platform_backend.py14
-rw-r--r--tools/telemetry/telemetry/core/platform/android_power_monitor.py157
-rw-r--r--tools/telemetry/telemetry/core/platform/android_power_monitor_unittest.py23
3 files changed, 194 insertions, 0 deletions
diff --git a/tools/telemetry/telemetry/core/platform/android_platform_backend.py b/tools/telemetry/telemetry/core/platform/android_platform_backend.py
index 58db52b..9a38698 100644
--- a/tools/telemetry/telemetry/core/platform/android_platform_backend.py
+++ b/tools/telemetry/telemetry/core/platform/android_platform_backend.py
@@ -10,6 +10,7 @@ from telemetry.core import bitmap
from telemetry.core import exceptions
from telemetry.core import platform
from telemetry.core import util
+from telemetry.core.platform import android_power_monitor
from telemetry.core.platform import proc_supporting_platform_backend
from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
@@ -45,6 +46,7 @@ class AndroidPlatformBackend(
self._host_platform_backend = platform.CreatePlatformBackendForCurrentOS()
self._can_access_protected_file_contents = \
self._adb.CanAccessProtectedFileContents()
+ self._powermonitor = android_power_monitor.PowerMonitorUtility(self._adb)
self._video_recorder = None
self._video_output = None
if self._no_performance_mode:
@@ -226,6 +228,18 @@ class AndroidPlatformBackend(
for frame in self._FramesFromMp4(self._video_output):
yield frame
+ def CanMonitorPowerAsync(self):
+ return self._powermonitor.CanMonitorPowerAsync()
+
+ def StartMonitoringPowerAsync(self):
+ self._powermonitor.StartMonitoringPowerAsync()
+
+ def StopMonitoringPowerAsync(self):
+ powermonitor_output = self._powermonitor.StopMonitoringPowerAsync()
+ assert powermonitor_output, 'PowerMonitor produced no output'
+ return android_power_monitor.PowerMonitorUtility.ParsePowerMetricsOutput(
+ powermonitor_output)
+
def _FramesFromMp4(self, mp4_file):
if not self.CanLaunchApplication('avconv'):
self.InstallApplication('avconv')
diff --git a/tools/telemetry/telemetry/core/platform/android_power_monitor.py b/tools/telemetry/telemetry/core/platform/android_power_monitor.py
new file mode 100644
index 0000000..a911f54
--- /dev/null
+++ b/tools/telemetry/telemetry/core/platform/android_power_monitor.py
@@ -0,0 +1,157 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import multiprocessing
+import os
+import tempfile
+import time
+
+
+SAMPLE_INTERVAL_S = 0.5 # 2 Hz. The data is collected from the ds2784 fuel gauge
+ # chip that only updates its data every 3.5s.
+FUEL_GAUGE_PATH = '/sys/class/power_supply/ds2784-fuelgauge'
+CHARGE_COUNTER = os.path.join(FUEL_GAUGE_PATH, 'charge_counter_ext')
+CURRENT = os.path.join(FUEL_GAUGE_PATH, 'current_now')
+VOLTAGE = os.path.join(FUEL_GAUGE_PATH, 'voltage_now')
+
+
+def _MonitorPower(adb, pipe, output):
+ """Monitoring process
+ Args:
+ pipe: socket used to notify the process to stop monitoring.
+ output: opened file to write the samples.
+ """
+ with output:
+ def _sample():
+ timestamp = time.time()
+ (charge, current, voltage) = adb.RunShellCommand(
+ 'cat %s;cat %s;cat %s' % (CHARGE_COUNTER, CURRENT, VOLTAGE),
+ log_result=False)
+ output.write('%f %s %s %s\n' % (timestamp, charge, current, voltage))
+ running = True
+ while running:
+ _sample()
+ running = not pipe.poll(SAMPLE_INTERVAL_S)
+ _sample()
+
+
+class PowerMonitorUtility(object):
+ def __init__(self, adb):
+ self._adb = adb
+ self._powermonitor_process = None
+ self._powermonitor_output_file = None
+ self._sending_pipe = None
+ self._has_fuel_gauge = None
+
+ def _IsDeviceCharging(self):
+ for line in self._adb.RunShellCommand('dumpsys battery'):
+ if 'powered: ' in line:
+ if 'true' == line.split('powered: ')[1]:
+ return True
+ return False
+
+ def CanMonitorPowerAsync(self):
+ if self._has_fuel_gauge is None:
+ self._has_fuel_gauge = self._adb.FileExistsOnDevice(CHARGE_COUNTER)
+ if not self._has_fuel_gauge:
+ return False
+ if self._IsDeviceCharging():
+ logging.warning('Can\'t monitor power usage since device is charging.')
+ return False
+ return True
+
+ def StartMonitoringPowerAsync(self):
+ assert not self._powermonitor_process, (
+ 'Must call StopMonitoringPowerAsync().')
+ self._powermonitor_output_file = tempfile.TemporaryFile()
+ (reception_pipe, self._sending_pipe) = multiprocessing.Pipe()
+ self._powermonitor_process = multiprocessing.Process(
+ target=_MonitorPower,
+ args=(self._adb,
+ reception_pipe,
+ self._powermonitor_output_file))
+ self._powermonitor_process.start()
+ reception_pipe.close()
+
+ def StopMonitoringPowerAsync(self):
+ assert self._powermonitor_process, (
+ 'StartMonitoringPowerAsync() not called.')
+ try:
+ # Tell powermonitor to take an immediate sample and join.
+ self._sending_pipe.send_bytes(' ')
+ self._powermonitor_process.join()
+ with self._powermonitor_output_file:
+ self._powermonitor_output_file.seek(0)
+ powermonitor_output = self._powermonitor_output_file.read()
+ return powermonitor_output
+ finally:
+ self._powermonitor_output_file = None
+ self._powermonitor_process = None
+ self._sending_pipe = None
+
+ @staticmethod
+ def ParsePowerMetricsOutput(powermonitor_output):
+ """Parse output of powermonitor command line utility.
+
+ Returns:
+ Dictionary in the format returned by StopMonitoringPowerAsync().
+ """
+ power_samples = []
+ total_energy_consumption_mwh = 0
+ def ParseSample(sample):
+ values = [float(x) for x in sample.split(' ')]
+ res = {}
+ (res['timestamp_s'],
+ res['charge_nah'],
+ res['current_ua'],
+ res['voltage_uv']) = values
+ return res
+ # The output contains a sample per line.
+ samples = map(ParseSample, powermonitor_output.split('\n')[:-1])
+ # Keep track of the last sample that found an updated reading.
+ last_updated_sample = samples[0]
+ # Compute average voltage.
+ voltage_sum_uv = 0
+ voltage_count = 0
+ for sample in samples:
+ if sample['charge_nah'] != last_updated_sample['charge_nah']:
+ charge_difference_nah = (sample['charge_nah'] -
+ last_updated_sample['charge_nah'])
+ # Use average voltage for the energy consumption.
+ voltage_sum_uv += sample['voltage_uv']
+ voltage_count += 1
+ average_voltage_uv = voltage_sum_uv / voltage_count
+ total_energy_consumption_mwh += (-charge_difference_nah *
+ average_voltage_uv / 10 ** 12)
+ last_updated_sample = sample
+ voltage_sum_uv = 0
+ voltage_count = 0
+ # Update average voltage.
+ voltage_sum_uv += sample['voltage_uv']
+ voltage_count += 1
+ # Compute energy of the sample.
+ energy_consumption_mw = (-sample['current_ua'] * sample['voltage_uv'] /
+ 10 ** 9)
+
+ power_samples.append(energy_consumption_mw)
+ # Because the data is stalled for a few seconds, compute the remaining
+ # energy consumption using the last available current reading.
+ last_sample = samples[-1]
+ remaining_time_h = (
+ last_sample['timestamp_s'] - last_updated_sample['timestamp_s']) / 3600
+ average_voltage_uv = voltage_sum_uv / voltage_count
+
+ remaining_energy_consumption_mwh = (-last_updated_sample['current_ua'] *
+ average_voltage_uv *
+ remaining_time_h / 10 ** 9)
+ total_energy_consumption_mwh += remaining_energy_consumption_mwh
+
+ # -------- Collect and Process Data -------------
+ out_dict = {}
+ # Raw power usage samples.
+ out_dict['power_samples_mw'] = power_samples
+ out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
+
+ return out_dict
diff --git a/tools/telemetry/telemetry/core/platform/android_power_monitor_unittest.py b/tools/telemetry/telemetry/core/platform/android_power_monitor_unittest.py
new file mode 100644
index 0000000..729849f
--- /dev/null
+++ b/tools/telemetry/telemetry/core/platform/android_power_monitor_unittest.py
@@ -0,0 +1,23 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+from telemetry.core.platform import android_power_monitor
+
+
+class AndroidPowerMonitorTest(unittest.TestCase):
+ def testEnergyComsumption(self):
+
+ data = ('0000 1000 -10 12\n'
+ '1800 1000 -10 11\n'
+ '3600 1000 -10 09\n'
+ '5400 0000 -20 08\n'
+ '7200 0000 -20 11\n'
+ '9000 0000 -20 11\n')
+ results = android_power_monitor.PowerMonitorUtility.ParsePowerMetricsOutput(
+ data)
+ self.assertEqual(results['power_samples_mw'], [1.2e-07, 1.1e-07, 9e-08,
+ 1.6e-07, 2.2e-07, 2.2e-07])
+ self.assertEqual(results['energy_consumption_mwh'], 2.1e-07)