Commit 9f4c1c95 authored by qsr@chromium.org's avatar qsr@chromium.org

Adding dumpsys based power monitor for android.

The implementation is based on the results from 'dumpsys batterystats'.

R=tonyg@chromium.org,jeremy@chromium.org
BUG=314481

Review URL: https://codereview.chromium.org/222413002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@261425 0039d316-1c4b-4281-b951-d872f2087c98
parent c31a8480
......@@ -12,7 +12,7 @@ from telemetry.unittest import options_for_unittests
class FakePlatform(object):
def IsRawDisplayFrameRateSupported(self):
return False
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
return False
......
......@@ -34,8 +34,8 @@ class PowerMetric(Metric):
if not self._running:
return
self._running = False
self._results = self._browser.platform.StopMonitoringPowerAsync()
if self._results: # StopMonitoringPowerAsync() can return None.
self._results = self._browser.platform.StopMonitoringPower()
if self._results: # StopMonitoringPower() can return None.
self._results['cpu_stats'] = (
_SubtractCpuStats(self._browser.cpu_stats, self._starting_cpu_stats))
......@@ -45,7 +45,7 @@ class PowerMetric(Metric):
# Friendly informational messages if measurement won't run.
system_supports_power_monitoring = (
factory.GetPlatformBackendForCurrentOS().CanMonitorPowerAsync())
factory.GetPlatformBackendForCurrentOS().CanMonitorPower())
if system_supports_power_monitoring:
if not PowerMetric.enabled:
logging.warning(
......@@ -58,7 +58,7 @@ class PowerMetric(Metric):
if not PowerMetric.enabled:
return
if not tab.browser.platform.CanMonitorPowerAsync():
if not tab.browser.platform.CanMonitorPower():
return
self._results = None
......@@ -67,14 +67,14 @@ class PowerMetric(Metric):
# This line invokes top a few times, call before starting power measurement.
self._starting_cpu_stats = self._browser.cpu_stats
self._browser.platform.StartMonitoringPowerAsync()
self._browser.platform.StartMonitoringPower(self._browser)
self._running = True
def Stop(self, _, tab):
if not PowerMetric.enabled:
return
if not tab.browser.platform.CanMonitorPowerAsync():
if not tab.browser.platform.CanMonitorPower():
return
self._StopInternal()
......@@ -116,6 +116,10 @@ def _SubtractCpuStats(cpu_stats, start_cpu_stats):
# Skip any process_types that are empty.
if (not cpu_stats[process_type]) or (not start_cpu_stats[process_type]):
continue
# Skip if IdleWakeupCount is not present.
if (('IdleWakeupCount' not in cpu_stats[process_type]) or
('IdleWakeupCount' not in start_cpu_stats[process_type])):
continue
idle_wakeup_delta = (cpu_stats[process_type]['IdleWakeupCount'] -
start_cpu_stats[process_type]['IdleWakeupCount'])
cpu_delta[process_type] = idle_wakeup_delta
......
......@@ -167,48 +167,23 @@ class Platform(object):
for t in self._platform_backend.StopVideoCapture():
yield t
def CanMonitorPowerSync(self):
"""Returns True iff power can be monitored synchronously via
MonitorPowerSync().
def CanMonitorPower(self):
"""Returns True iff power can be monitored asynchronously via
StartMonitoringPower() and StopMonitoringPower().
"""
return self._platform_backend.CanMonitorPowerSync()
def MonitorPowerSync(self, duration_ms):
"""Synchronously monitors power for |duration_ms|.
return self._platform_backend.CanMonitorPower()
Returns:
A dict of power utilization statistics containing: {
# The instantaneous power (voltage * current) reading in milliwatts at
# each sample.
'power_samples_mw': [mw0, mw1, ..., mwN],
def StartMonitoringPower(self, browser):
"""Starts monitoring power utilization statistics.
# The total energy consumption during the sampling period in milliwatt
# hours. May be estimated by integrating power samples or may be exact
# on supported hardware.
'energy_consumption_mwh': mwh,
# A platform-specific dictionary of additional details about the
# utilization of individual hardware components.
hw_component_utilization: {
...
}
}
"""
return self._platform_backend.MonitorPowerSync(duration_ms)
def CanMonitorPowerAsync(self):
"""Returns True iff power can be monitored asynchronously via
StartMonitoringPowerAsync() and StopMonitoringPowerAsync().
Args:
browser: The browser to monitor.
"""
return self._platform_backend.CanMonitorPowerAsync()
def StartMonitoringPowerAsync(self):
"""Starts monitoring power utilization statistics."""
assert self._platform_backend.CanMonitorPowerAsync()
self._platform_backend.StartMonitoringPowerAsync()
assert self._platform_backend.CanMonitorPower()
self._platform_backend.StartMonitoringPower(browser)
def StopMonitoringPowerAsync(self):
"""Stops monitoring power utilization and returns collects stats
def StopMonitoringPower(self):
"""Stops monitoring power utilization and returns stats
Returns:
None if power measurement failed for some reason, otherwise a dict of
......@@ -233,4 +208,4 @@ class Platform(object):
}
}
"""
return self._platform_backend.StopMonitoringPowerAsync()
return self._platform_backend.StopMonitoringPower()
......@@ -14,6 +14,7 @@ from telemetry.core import util
from telemetry.core.platform import proc_supporting_platform_backend
from telemetry.core.platform import factory
from telemetry.core.platform.power_monitor import android_ds2784_power_monitor
from telemetry.core.platform.power_monitor import android_dumpsys_power_monitor
from telemetry.core.platform.power_monitor import monsoon_power_monitor
from telemetry.core.platform.power_monitor import power_monitor_controller
from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
......@@ -52,7 +53,8 @@ class AndroidPlatformBackend(
self._adb.CanAccessProtectedFileContents()
self._powermonitor = power_monitor_controller.PowerMonitorController([
monsoon_power_monitor.MonsoonPowerMonitor(),
android_ds2784_power_monitor.DS2784PowerMonitor(adb)
android_ds2784_power_monitor.DS2784PowerMonitor(adb),
android_dumpsys_power_monitor.DumpsysPowerMonitor(adb),
])
self._video_recorder = None
self._video_output = None
......@@ -250,14 +252,14 @@ class AndroidPlatformBackend(
for frame in self._FramesFromMp4(self._video_output):
yield frame
def CanMonitorPowerAsync(self):
return self._powermonitor.CanMonitorPowerAsync()
def CanMonitorPower(self):
return self._powermonitor.CanMonitorPower()
def StartMonitoringPowerAsync(self):
self._powermonitor.StartMonitoringPowerAsync()
def StartMonitoringPower(self, browser):
self._powermonitor.StartMonitoringPower(browser)
def StopMonitoringPowerAsync(self):
return self._powermonitor.StopMonitoringPowerAsync()
def StopMonitoringPower(self):
return self._powermonitor.StopMonitoringPower()
def _FramesFromMp4(self, mp4_file):
if not self.CanLaunchApplication('avconv'):
......
......@@ -158,11 +158,11 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
p.communicate()
assert p.returncode == 0, 'Failed to flush system cache'
def CanMonitorPowerAsync(self):
return self.power_monitor_.CanMonitorPowerAsync()
def CanMonitorPower(self):
return self.power_monitor_.CanMonitorPower()
def StartMonitoringPowerAsync(self):
self.power_monitor_.StartMonitoringPowerAsync()
def StartMonitoringPower(self, browser):
self.power_monitor_.StartMonitoringPower(browser)
def StopMonitoringPowerAsync(self):
return self.power_monitor_.StopMonitoringPowerAsync()
def StopMonitoringPower(self):
return self.power_monitor_.StopMonitoringPower()
......@@ -2,8 +2,6 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import time
# pylint: disable=W0613
......@@ -123,19 +121,11 @@ class PlatformBackend(object):
def StopVideoCapture(self):
raise NotImplementedError()
def CanMonitorPowerSync(self):
return self.CanMonitorPowerAsync()
def MonitorPowerSync(self, duration_ms):
self.StartMonitoringPowerAsync()
time.sleep(duration_ms / 1000.)
return self.StopMonitoringPowerAsync()
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
return False
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
raise NotImplementedError()
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
raise NotImplementedError()
......@@ -3,6 +3,7 @@
# found in the LICENSE file.
import logging
import time
import unittest
from telemetry.core.platform import factory
......@@ -12,10 +13,20 @@ class PlatformBackendTest(unittest.TestCase):
def testPowerMonitoringSync(self):
# Tests that the act of monitoring power doesn't blow up.
backend = factory.GetPlatformBackendForCurrentOS()
if not backend.CanMonitorPowerSync():
if not backend.CanMonitorPower():
logging.warning('Test not supported on this platform.')
return
output = backend.MonitorPowerSync(1)
browser_mock = lambda: None
# Android needs to access the package of the monitored app.
if backend.GetOSName() == 'android':
# pylint: disable=W0212
browser_mock._browser_backend = lambda: None
# Monitor the launcher, which is always present.
browser_mock._browser_backend.package = 'com.android.launcher'
backend.StartMonitoringPower(browser_mock)
time.sleep(0.001)
output = backend.StopMonitoringPower()
self.assertTrue(output.has_key('power_samples_mw'))
self.assertTrue(output.has_key('identifier'))
......@@ -10,19 +10,22 @@ class PowerMonitor(object):
Provides an interface to register power consumption during a test.
"""
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
"""Returns True iff power can be monitored asynchronously via
StartMonitoringPowerAsync() and StopMonitoringPowerAsync().
StartMonitoringPower() and StopMonitoringPower().
"""
return False
def StartMonitoringPowerAsync(self):
"""Starts monitoring power utilization statistics."""
def StartMonitoringPower(self, browser):
"""Starts monitoring power utilization statistics.
See Platform#StartMonitoringPower for the arguments format.
"""
raise NotImplementedError()
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
"""Stops monitoring power utilization and returns collects stats
See Platform#StopMonitoringPowerAsync for the return format.
See Platform#StopMonitoringPower for the return format.
"""
raise NotImplementedError()
......@@ -39,7 +39,7 @@ class DS2784PowerMonitor(power_monitor.PowerMonitor):
def _HasFuelGauge(self):
return self._adb.FileExistsOnDevice(CHARGE_COUNTER)
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
if not self._HasFuelGauge():
return False
if self._IsDeviceCharging():
......@@ -47,16 +47,16 @@ class DS2784PowerMonitor(power_monitor.PowerMonitor):
return False
return True
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
assert not self._powermonitor_process_port, (
'Must call StopMonitoringPowerAsync().')
'Must call StopMonitoringPower().')
self._powermonitor_process_port = int(self._adb.RunShellCommand(
'%s %d %s %s %s' % (self._file_poller_binary, SAMPLE_RATE_HZ,
CHARGE_COUNTER, CURRENT, VOLTAGE))[0])
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
assert self._powermonitor_process_port, (
'StartMonitoringPowerAsync() not called.')
'StartMonitoringPower() not called.')
try:
result = '\n'.join(self._adb.RunShellCommand(
'%s %d' % (self._file_poller_binary,
......@@ -71,7 +71,7 @@ class DS2784PowerMonitor(power_monitor.PowerMonitor):
"""Parse output of powermonitor command line utility.
Returns:
Dictionary in the format returned by StopMonitoringPowerAsync().
Dictionary in the format returned by StopMonitoringPower().
"""
power_samples = []
total_energy_consumption_mwh = 0
......
# 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 telemetry.core.platform.power_monitor as power_monitor
import csv
from collections import defaultdict
class DumpsysPowerMonitor(power_monitor.PowerMonitor):
"""PowerMonitor that relies on the dumpsys batterystats to monitor the power
consumption of a single android application. This measure uses a heuristic
and is the same information end-users see with the battery application.
"""
def __init__(self, adb):
"""Constructor.
Args:
adb: adb proxy.
"""
super(DumpsysPowerMonitor, self).__init__()
self._adb = adb
self._browser = None
def CanMonitorPower(self):
return self._adb.CanControlUsbCharging()
def StartMonitoringPower(self, browser):
assert not self._browser, (
'Must call StopMonitoringPower().')
self._browser = browser
# Disable the charging of the device over USB. This is necessary because the
# device only collects information about power usage when the device is not
# charging.
self._adb.DisableUsbCharging()
def StopMonitoringPower(self):
assert self._browser, (
'StartMonitoringPower() not called.')
try:
self._adb.EnableUsbCharging()
# pylint: disable=W0212
package = self._browser._browser_backend.package
# By default, 'dumpsys batterystats' measures power consumption during the
# last unplugged period.
result = self._adb.RunShellCommand('dumpsys batterystats -c %s' % package)
assert result, 'Dumpsys produced no output'
return DumpsysPowerMonitor.ParseSamplingOutput(package, result)
finally:
self._browser = None
@staticmethod
def ParseSamplingOutput(package, dumpsys_output):
"""Parse output of 'dumpsys batterystats -c'
Returns:
Dictionary in the format returned by StopMonitoringPower().
"""
# csv columns
DUMP_VERSION_INDEX = 0
COLUMN_TYPE_INDEX = 3
PACKAGE_UID_INDEX = 4
PWI_POWER_COMSUMPTION_INDEX = 5
PWI_UID_INDEX = 1
PWI_AGGREGATION_INDEX = 2
PWI_SUBTYPE_INDEX = 4
csvreader = csv.reader(dumpsys_output)
entries_by_type = defaultdict(list)
for entry in csvreader:
if len(entry) < 4 or entry[DUMP_VERSION_INDEX] != '7':
continue
entries_by_type[entry[COLUMN_TYPE_INDEX]].append(entry)
# Find the uid of for the given package.
assert package in entries_by_type, 'Expected package not found'
assert len(entries_by_type[package]) == 1, 'Multiple entries for package.'
uid = entries_by_type[package][0][PACKAGE_UID_INDEX]
consumptions_mah = [float(entry[PWI_POWER_COMSUMPTION_INDEX])
for entry in entries_by_type['pwi']
if entry[PWI_UID_INDEX] == uid and
entry[PWI_AGGREGATION_INDEX] == 't' and
entry[PWI_SUBTYPE_INDEX] == 'uid']
consumption_mah = sum(consumptions_mah)
# Converting at a nominal voltage of 4.0V, as those values are obtained by a
# heuristic, and 4.0V is the voltage we set when using a monsoon device.
consumption_mwh = consumption_mah * 4.0
# Raw power usage samples.
out_dict = {}
out_dict['identifier'] = 'dumpsys'
out_dict['power_samples_mw'] = []
out_dict['energy_consumption_mwh'] = consumption_mwh
return out_dict
# 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 os
import unittest
from telemetry.core.platform.power_monitor import android_dumpsys_power_monitor
from telemetry.core.util import GetUnittestDataDir
class DS2784PowerMonitorMonitorTest(unittest.TestCase):
def testEnergyComsumption(self):
package = 'com.google.android.apps.chrome'
dumpsys_output = os.path.join(GetUnittestDataDir(), 'batterystats_v7.csv')
with open(dumpsys_output, 'r') as output:
results = (
android_dumpsys_power_monitor.DumpsysPowerMonitor.ParseSamplingOutput(
package, output))
self.assertEqual(results['identifier'], 'dumpsys')
self.assertAlmostEqual(results['energy_consumption_mwh'], 2.924)
if __name__ == '__main__':
unittest.main()
......@@ -59,12 +59,12 @@ class MonsoonPowerMonitor(power_monitor.PowerMonitor):
except EnvironmentError:
self._monsoon = None
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
return self._monsoon is not None
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
assert not self._powermonitor_process, (
'Must call StopMonitoringPowerAsync().')
'Must call StopMonitoringPower().')
self._powermonitor_output_file = tempfile.TemporaryFile()
self._is_collecting = multiprocessing.Event()
self._powermonitor_process = multiprocessing.Process(
......@@ -77,9 +77,9 @@ class MonsoonPowerMonitor(power_monitor.PowerMonitor):
self._powermonitor_process.terminate()
raise exceptions.ProfilingException('Failed to start data collection.')
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
assert self._powermonitor_process, (
'StartMonitoringPowerAsync() not called.')
'StartMonitoringPower() not called.')
try:
# Tell powermonitor to take an immediate sample and join.
self._is_collecting.clear()
......@@ -99,7 +99,7 @@ class MonsoonPowerMonitor(power_monitor.PowerMonitor):
"""Parse the output of of the samples collector process.
Returns:
Dictionary in the format returned by StopMonitoringPowerAsync().
Dictionary in the format returned by StopMonitoringPower().
"""
power_samples = []
total_energy_consumption_mwh = 0
......
......@@ -17,20 +17,20 @@ class PowerMonitorController(power_monitor.PowerMonitor):
def _AsyncPowerMonitor(self):
return next(
(x for x in self._cascading_power_monitors if x.CanMonitorPowerAsync()),
(x for x in self._cascading_power_monitors if x.CanMonitorPower()),
None)
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
return bool(self._AsyncPowerMonitor())
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
self._active_monitor = self._AsyncPowerMonitor()
assert self._active_monitor, 'No available monitor.'
self._active_monitor.StartMonitoringPowerAsync()
self._active_monitor.StartMonitoringPower(browser)
def StopMonitoringPowerAsync(self):
assert self._active_monitor, 'StartMonitoringPowerAsync() not called.'
def StopMonitoringPower(self):
assert self._active_monitor, 'StartMonitoringPower() not called.'
try:
return self._active_monitor.StopMonitoringPowerAsync()
return self._active_monitor.StopMonitoringPower()
finally:
self._active_monitor = None
......@@ -12,24 +12,24 @@ class PowerMonitorControllerTest(unittest.TestCase):
def testComposition(self):
class P1(power_monitor.PowerMonitor):
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
raise NotImplementedError()
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
raise NotImplementedError()
class P2(power_monitor.PowerMonitor):
def __init__(self, value):
super(P2, self).__init__()
self._value = value
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
return True
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
pass
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
return self._value
controller = power_monitor_controller.PowerMonitorController(
[P1(), P2(1), P2(2)])
self.assertEqual(controller.CanMonitorPowerAsync(), True)
controller.StartMonitoringPowerAsync()
self.assertEqual(controller.StopMonitoringPowerAsync(), 1)
self.assertEqual(controller.CanMonitorPower(), True)
controller.StartMonitoringPower(None)
self.assertEqual(controller.StopMonitoringPower(), 1)
......@@ -29,9 +29,9 @@ class PowerMetricsPowerMonitor(power_monitor.PowerMonitor):
def binary_path(self):
return '/usr/bin/powermetrics'
def StartMonitoringPowerAsync(self):
def StartMonitoringPower(self, browser):
assert not self._powermetrics_process, (
"Must call StopMonitoringPowerAsync().")
"Must call StopMonitoringPower().")
SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary.
# Empirically powermetrics creates an empty output file immediately upon
# starting. We detect file creation as a signal that measurement has
......@@ -56,7 +56,7 @@ class PowerMetricsPowerMonitor(power_monitor.PowerMonitor):
util.WaitFor(_OutputFileExists, timeout_sec)
@decorators.Cache
def CanMonitorPowerAsync(self):
def CanMonitorPower(self):
mavericks_or_later = (self._backend.GetOSVersionName() >=
platform.mac_platform_backend.MAVERICKS)
binary_path = self.binary_path
......@@ -84,7 +84,7 @@ class PowerMetricsPowerMonitor(power_monitor.PowerMonitor):
"""Parse output of powermetrics command line utility.
Returns:
Dictionary in the format returned by StopMonitoringPowerAsync() or None
Dictionary in the format returned by StopMonitoringPower() or None
if |powermetrics_output| is empty - crbug.com/353250 .
"""
if len(powermetrics_output) == 0:
......@@ -236,9 +236,9 @@ class PowerMetricsPowerMonitor(power_monitor.PowerMonitor):
StoreMetricAverage(m, sample_durations, out_dict)
return out_dict
def StopMonitoringPowerAsync(self):
def StopMonitoringPower(self):
assert self._powermetrics_process, (
"StartMonitoringPowerAsync() not called.")
"StartMonitoringPower() not called.")
# Tell powermetrics to take an immediate sample.
try:
self._powermetrics_process.send_signal(signal.SIGINFO)
......
......@@ -20,7 +20,7 @@ class PowerMetricsPowerMonitorTest(unittest.TestCase):
mavericks_or_later = (
backend.GetOSVersionName() >= mac_platform_backend.MAVERICKS)
# Should always be able to monitor power usage on OS Version >= 10.9 .
self.assertEqual(power_monitor.CanMonitorPowerAsync(), mavericks_or_later,
self.assertEqual(power_monitor.CanMonitorPower(), mavericks_or_later,
"Error checking powermetrics availability: '%s'" % '|'.join(os.uname()))
@test.Enabled('mac')
......@@ -40,7 +40,7 @@ class PowerMetricsPowerMonitorTest(unittest.TestCase):
power_monitor = powermetrics_power_monitor.PowerMetricsPowerMonitor(
mac_platform_backend.MacPlatformBackend())
if not power_monitor.CanMonitorPowerAsync():
if not power_monitor.CanMonitorPower():
logging.warning('Test not supported on this platform.')
return
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment