diff options
author | qsr@chromium.org <qsr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-02-07 21:24:19 +0000 |
---|---|---|
committer | qsr@chromium.org <qsr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-02-07 21:24:19 +0000 |
commit | bcc75b8a94f273590d12ec930d6afb7fc3b66036 (patch) | |
tree | fab94b0c658db9c2b912ff4808fda02e3f83e382 | |
parent | 4259ce953309b6454b572af71626e1bb5a82934d (diff) | |
download | chromium_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
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) |