982 lines
40 KiB
Python
Executable File
982 lines
40 KiB
Python
Executable File
# Copyright (c) 2012 The Chromium OS 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 collections
|
|
import json
|
|
import logging
|
|
import numpy
|
|
import os
|
|
import re
|
|
import time
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import utils as _utils
|
|
from autotest_lib.client.common_lib.cros import arc
|
|
from autotest_lib.client.common_lib.cros import arc_common
|
|
from autotest_lib.client.common_lib.cros import chrome
|
|
from autotest_lib.client.common_lib.cros import power_load_util
|
|
from autotest_lib.client.common_lib.cros.network import interface
|
|
from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
|
|
from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
|
|
from autotest_lib.client.cros import backchannel
|
|
from autotest_lib.client.cros import ec
|
|
from autotest_lib.client.cros import httpd
|
|
from autotest_lib.client.cros import memory_bandwidth_logger
|
|
from autotest_lib.client.cros import service_stopper
|
|
from autotest_lib.client.cros.audio import audio_helper
|
|
from autotest_lib.client.cros.networking import cellular_proxy
|
|
from autotest_lib.client.cros.networking import shill_proxy
|
|
from autotest_lib.client.cros.networking import wifi_proxy
|
|
from autotest_lib.client.cros.power import power_dashboard
|
|
from autotest_lib.client.cros.power import power_status
|
|
from autotest_lib.client.cros.power import power_utils
|
|
from telemetry.core import exceptions
|
|
|
|
params_dict = {
|
|
'test_time_ms': '_mseconds',
|
|
'should_scroll': '_should_scroll',
|
|
'should_scroll_up': '_should_scroll_up',
|
|
'scroll_loop': '_scroll_loop',
|
|
'scroll_interval_ms': '_scroll_interval_ms',
|
|
'scroll_by_pixels': '_scroll_by_pixels',
|
|
'tasks': '_tasks',
|
|
}
|
|
|
|
class power_LoadTest(arc.ArcTest):
|
|
"""test class"""
|
|
version = 2
|
|
|
|
def initialize(self, percent_initial_charge_min=None,
|
|
check_network=True, loop_time=3600, loop_count=1,
|
|
should_scroll='true', should_scroll_up='true',
|
|
scroll_loop='false', scroll_interval_ms='10000',
|
|
scroll_by_pixels='600', test_low_batt_p=3,
|
|
verbose=True, force_wifi=False, wifi_ap='', wifi_sec='none',
|
|
wifi_pw='', wifi_timeout=60, use_cellular_network=False,
|
|
tasks='', volume_level=10, mic_gain=10, low_batt_margin_p=2,
|
|
ac_ok=False, log_mem_bandwidth=False, gaia_login=None,
|
|
force_discharge=False, pdash_note=''):
|
|
"""
|
|
percent_initial_charge_min: min battery charge at start of test
|
|
check_network: check that Ethernet interface is not running
|
|
loop_time: length of time to run the test for in each loop
|
|
loop_count: number of times to loop the test for
|
|
should_scroll: should the extension scroll pages
|
|
should_scroll_up: should scroll in up direction
|
|
scroll_loop: continue scrolling indefinitely
|
|
scroll_interval_ms: how often to scoll
|
|
scroll_by_pixels: number of pixels to scroll each time
|
|
test_low_batt_p: percent battery at which test should stop
|
|
verbose: add more logging information
|
|
force_wifi: should we force to test to run on wifi
|
|
wifi_ap: the name (ssid) of the wifi access point
|
|
wifi_sec: the type of security for the wifi ap
|
|
wifi_pw: password for the wifi ap
|
|
wifi_timeout: The timeout for wifi configuration
|
|
use_cellular_network: use the cellular network connection instead of wifi
|
|
volume_level: percent audio volume level
|
|
mic_gain: percent audio microphone gain level
|
|
low_batt_margin_p: percent low battery margin to be added to
|
|
sys_low_batt_p to guarantee test completes prior to powerd shutdown
|
|
ac_ok: boolean to allow running on AC
|
|
log_mem_bandwidth: boolean to log memory bandwidth during the test
|
|
gaia_login: whether real GAIA login should be attempted. If 'None'
|
|
(default) then boolean is determined from URL.
|
|
force_discharge: boolean of whether to tell ec to discharge battery even
|
|
when the charger is plugged in.
|
|
pdash_note: note of the current run to send to power dashboard.
|
|
"""
|
|
self._backlight = None
|
|
self._services = None
|
|
self._browser = None
|
|
self._loop_time = loop_time
|
|
self._loop_count = loop_count
|
|
self._mseconds = self._loop_time * 1000
|
|
self._verbose = verbose
|
|
|
|
self._sys_low_batt_p = 0.
|
|
self._sys_low_batt_s = 0.
|
|
self._test_low_batt_p = test_low_batt_p
|
|
self._should_scroll = should_scroll
|
|
self._should_scroll_up = should_scroll_up
|
|
self._scroll_loop = scroll_loop
|
|
self._scroll_interval_ms = scroll_interval_ms
|
|
self._scroll_by_pixels = scroll_by_pixels
|
|
self._tmp_keyvals = {}
|
|
self._power_status = None
|
|
self._force_wifi = force_wifi
|
|
self._use_cellular_network = use_cellular_network
|
|
self._testServer = None
|
|
self._tasks = tasks.replace(' ','')
|
|
self._backchannel = None
|
|
self._shill_proxy = None
|
|
self._volume_level = volume_level
|
|
self._mic_gain = mic_gain
|
|
self._ac_ok = ac_ok
|
|
self._log_mem_bandwidth = log_mem_bandwidth
|
|
self._wait_time = 60
|
|
self._stats = collections.defaultdict(list)
|
|
self._force_discharge = force_discharge
|
|
self._pdash_note = pdash_note
|
|
|
|
self._power_status = power_status.get_status()
|
|
|
|
if force_discharge:
|
|
if not self._power_status.battery:
|
|
raise error.TestNAError('DUT does not have battery. '
|
|
'Could not force discharge.')
|
|
if not ec.has_cros_ec():
|
|
raise error.TestNAError('DUT does not have CrOS EC. '
|
|
'Could not force discharge.')
|
|
if not power_utils.charge_control_by_ectool(False):
|
|
raise error.TestError('Could not run battery force discharge.')
|
|
self._ac_ok = True
|
|
|
|
if not self._power_status.battery:
|
|
if ac_ok and (power_utils.has_powercap_support() or
|
|
power_utils.has_rapl_support()):
|
|
logging.info("Device has no battery but has powercap data.")
|
|
else:
|
|
rsp = "Skipping test for device without battery and powercap."
|
|
raise error.TestNAError(rsp)
|
|
|
|
self._tmp_keyvals['b_on_ac'] = (not force_discharge and
|
|
self._power_status.on_ac())
|
|
|
|
self._gaia_login = gaia_login
|
|
if gaia_login is None:
|
|
self._gaia_login = power_load_util.use_gaia_login()
|
|
|
|
self._username = power_load_util.get_username()
|
|
self._password = power_load_util.get_password()
|
|
|
|
if not self._ac_ok:
|
|
self._power_status.assert_battery_state(percent_initial_charge_min)
|
|
|
|
# If force wifi enabled, convert eth0 to backchannel and connect to the
|
|
# specified WiFi AP.
|
|
if self._force_wifi:
|
|
if self._use_cellular_network:
|
|
raise error.TestError("Can't force WiFi AP when cellular network"
|
|
"is used");
|
|
|
|
sec_config = None
|
|
# TODO(dbasehore): Fix this when we get a better way of figuring out
|
|
# the wifi security configuration.
|
|
if wifi_sec == 'rsn' or wifi_sec == 'wpa':
|
|
sec_config = xmlrpc_security_types.WPAConfig(
|
|
psk=wifi_pw,
|
|
wpa_mode=xmlrpc_security_types.WPAConfig.MODE_PURE_WPA2,
|
|
wpa2_ciphers=
|
|
[xmlrpc_security_types.WPAConfig.CIPHER_CCMP])
|
|
wifi_config = xmlrpc_datatypes.AssociationParameters(
|
|
ssid=wifi_ap, security_config=sec_config,
|
|
configuration_timeout=wifi_timeout)
|
|
# If backchannel is already running, don't run it again.
|
|
self._backchannel = backchannel.Backchannel()
|
|
if not self._backchannel.setup():
|
|
raise error.TestError('Could not setup Backchannel network.')
|
|
|
|
self._shill_proxy = wifi_proxy.WifiProxy()
|
|
self._shill_proxy.remove_all_wifi_entries()
|
|
for i in xrange(1,4):
|
|
raw_output = self._shill_proxy.connect_to_wifi_network(
|
|
wifi_config.ssid,
|
|
wifi_config.security,
|
|
wifi_config.security_parameters,
|
|
wifi_config.save_credentials,
|
|
station_type=wifi_config.station_type,
|
|
hidden_network=wifi_config.is_hidden,
|
|
discovery_timeout_seconds=
|
|
wifi_config.discovery_timeout,
|
|
association_timeout_seconds=
|
|
wifi_config.association_timeout,
|
|
configuration_timeout_seconds=
|
|
wifi_config.configuration_timeout * i)
|
|
result = xmlrpc_datatypes.AssociationResult. \
|
|
from_dbus_proxy_output(raw_output)
|
|
if result.success:
|
|
break
|
|
logging.warn('wifi connect: disc:%d assoc:%d config:%d fail:%s',
|
|
result.discovery_time, result.association_time,
|
|
result.configuration_time, result.failure_reason)
|
|
else:
|
|
raise error.TestError('Could not connect to WiFi network.')
|
|
|
|
else:
|
|
# Find all wired ethernet interfaces.
|
|
ifaces = [ iface for iface in interface.get_interfaces()
|
|
if (not iface.is_wifi_device() and
|
|
iface.name.startswith('eth')) ]
|
|
logging.debug(str([iface.name for iface in ifaces]))
|
|
for iface in ifaces:
|
|
if check_network and iface.is_lower_up:
|
|
raise error.TestError('Ethernet interface is active. ' +
|
|
'Please remove Ethernet cable')
|
|
|
|
if self._use_cellular_network:
|
|
self._shill_proxy = cellular_proxy.CellularProxy()
|
|
cdev = self._shill_proxy.find_cellular_device_object()
|
|
if cdev is None:
|
|
raise error.TestError("No cellular device found")
|
|
|
|
self._shill_proxy.manager.DisableTechnology(
|
|
shill_proxy.ShillProxy.TECHNOLOGY_WIFI)
|
|
|
|
self._shill_proxy.wait_for_cellular_service_object()
|
|
|
|
# record the max backlight level
|
|
self._backlight = power_utils.Backlight()
|
|
self._tmp_keyvals['level_backlight_max'] = \
|
|
self._backlight.get_max_level()
|
|
|
|
self._services = service_stopper.ServiceStopper(
|
|
service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
|
|
self._services.stop_services()
|
|
|
|
self._detachable_handler = power_utils.BaseActivitySimulator()
|
|
|
|
# fix up file perms for the power test extension so that chrome
|
|
# can access it
|
|
os.system('chmod -R 755 %s' % self.bindir)
|
|
|
|
# setup a HTTP Server to listen for status updates from the power
|
|
# test extension
|
|
self._testServer = httpd.HTTPListener(8001, docroot=self.bindir)
|
|
self._testServer.run()
|
|
|
|
# initialize various interesting power related stats
|
|
self._statomatic = power_status.StatoMatic()
|
|
|
|
self._power_status.refresh()
|
|
self._sys_low_batt_p = float(utils.system_output(
|
|
'check_powerd_config --low_battery_shutdown_percent'))
|
|
self._sys_low_batt_s = int(utils.system_output(
|
|
'check_powerd_config --low_battery_shutdown_time'))
|
|
|
|
if self._sys_low_batt_p and self._sys_low_batt_s:
|
|
raise error.TestError(
|
|
"Low battery percent and seconds are non-zero.")
|
|
|
|
min_low_batt_p = min(self._sys_low_batt_p + low_batt_margin_p, 100)
|
|
if self._sys_low_batt_p and (min_low_batt_p > self._test_low_batt_p):
|
|
logging.warning("test low battery threshold is below system " +
|
|
"low battery requirement. Setting to %f",
|
|
min_low_batt_p)
|
|
self._test_low_batt_p = min_low_batt_p
|
|
|
|
if self._power_status.battery:
|
|
self._ah_charge_start = self._power_status.battery.charge_now
|
|
self._wh_energy_start = self._power_status.battery.energy
|
|
|
|
self.task_monitor_file = open(os.path.join(self.resultsdir,
|
|
'task-monitor.json'), 'wt')
|
|
|
|
|
|
def run_once(self):
|
|
"""Test main loop."""
|
|
t0 = time.time()
|
|
|
|
# record the PSR related info.
|
|
psr = power_utils.DisplayPanelSelfRefresh(init_time=t0)
|
|
|
|
try:
|
|
self._keyboard_backlight = power_utils.KbdBacklight()
|
|
self._set_keyboard_backlight_level()
|
|
except power_utils.KbdBacklightException as e:
|
|
logging.info("Assuming no keyboard backlight due to :: %s", str(e))
|
|
self._keyboard_backlight = None
|
|
|
|
self._checkpoint_logger = power_status.CheckpointLogger()
|
|
seconds_period = 20.0
|
|
self._meas_logs = power_status.create_measurement_loggers(
|
|
seconds_period, self._checkpoint_logger)
|
|
for log in self._meas_logs:
|
|
log.start()
|
|
if self._log_mem_bandwidth:
|
|
self._mlog = memory_bandwidth_logger.MemoryBandwidthLogger(
|
|
raw=False, seconds_period=2)
|
|
self._mlog.start()
|
|
|
|
# record start time and end time for each task
|
|
self._task_tracker = []
|
|
|
|
ext_path = os.path.join(os.path.dirname(__file__), 'extension')
|
|
self._tmp_keyvals['username'] = self._username
|
|
|
|
arc_mode = arc_common.ARC_MODE_DISABLED
|
|
if utils.is_arc_available():
|
|
arc_mode = arc_common.ARC_MODE_ENABLED
|
|
|
|
try:
|
|
self._browser = chrome.Chrome(extension_paths=[ext_path],
|
|
gaia_login=self._gaia_login,
|
|
username=self._username,
|
|
password=self._password,
|
|
arc_mode=arc_mode)
|
|
except exceptions.LoginException:
|
|
# already failed guest login
|
|
if not self._gaia_login:
|
|
raise
|
|
self._gaia_login = False
|
|
logging.warn("Unable to use GAIA acct %s. Using GUEST instead.\n",
|
|
self._username)
|
|
self._browser = chrome.Chrome(extension_paths=[ext_path],
|
|
gaia_login=self._gaia_login)
|
|
if not self._gaia_login:
|
|
self._tmp_keyvals['username'] = 'GUEST'
|
|
|
|
extension = self._browser.get_extension(ext_path)
|
|
for k in params_dict:
|
|
if getattr(self, params_dict[k]) is not '':
|
|
extension.ExecuteJavaScript('var %s = %s;' %
|
|
(k, getattr(self, params_dict[k])))
|
|
|
|
# This opens a trap start page to capture tabs opened for first login.
|
|
# It will be closed when startTest is run.
|
|
extension.ExecuteJavaScript('chrome.windows.create(null, null);')
|
|
|
|
for i in range(self._loop_count):
|
|
start_time = time.time()
|
|
extension.ExecuteJavaScript('startTest();')
|
|
# the power test extension will report its status here
|
|
latch = self._testServer.add_wait_url('/status')
|
|
|
|
# this starts a thread in the server that listens to log
|
|
# information from the script
|
|
script_logging = self._testServer.add_wait_url(url='/log')
|
|
|
|
# dump any log entry that comes from the script into
|
|
# the debug log
|
|
self._testServer.add_url_handler(url='/log',\
|
|
handler_func=(lambda handler, forms, loop_counter=i:\
|
|
_extension_log_handler(handler, forms, loop_counter)))
|
|
|
|
pagetime_tracking = self._testServer.add_wait_url(url='/pagetime')
|
|
|
|
self._testServer.add_url_handler(url='/pagetime',\
|
|
handler_func=(lambda handler, forms, test_instance=self,
|
|
loop_counter=i:\
|
|
_extension_page_time_info_handler(handler, forms,
|
|
loop_counter,
|
|
test_instance)))
|
|
|
|
keyvalues_tracking = self._testServer.add_wait_url(url='/keyvalues')
|
|
|
|
self._testServer.add_url_handler(url='/keyvalues',\
|
|
handler_func=(lambda handler, forms, test_instance=self,
|
|
loop_counter=i:\
|
|
_extension_key_values_handler(handler, forms,
|
|
loop_counter,
|
|
test_instance)))
|
|
self._testServer.add_url(url='/task-monitor')
|
|
self._testServer.add_url_handler(
|
|
url='/task-monitor',
|
|
handler_func=lambda handler, forms:
|
|
self._extension_task_monitor_handler(handler, forms)
|
|
)
|
|
|
|
# setup a handler to simulate waking up the base of a detachable
|
|
# on user interaction. On scrolling, wake for 1s, on page
|
|
# navigation, wake for 10s.
|
|
self._testServer.add_url(url='/pagenav')
|
|
self._testServer.add_url(url='/scroll')
|
|
|
|
self._testServer.add_url_handler(url='/pagenav',
|
|
handler_func=(lambda handler, args, plt=self:
|
|
plt._detachable_handler.wake_base(10000)))
|
|
|
|
self._testServer.add_url_handler(url='/scroll',
|
|
handler_func=(lambda handler, args, plt=self:
|
|
plt._detachable_handler.wake_base(1000)))
|
|
# reset backlight level since powerd might've modified it
|
|
# based on ambient light
|
|
self._set_backlight_level(i)
|
|
self._set_lightbar_level()
|
|
if self._keyboard_backlight:
|
|
self._set_keyboard_backlight_level(loop=i)
|
|
audio_helper.set_volume_levels(self._volume_level,
|
|
self._mic_gain)
|
|
|
|
low_battery = self._do_wait(self._verbose, self._loop_time,
|
|
latch)
|
|
script_logging.set()
|
|
pagetime_tracking.set()
|
|
keyvalues_tracking.set()
|
|
|
|
self._log_loop_checkpoint(i, start_time, time.time())
|
|
|
|
if self._verbose:
|
|
logging.debug('loop %d completed', i)
|
|
|
|
if low_battery:
|
|
logging.info('Exiting due to low battery')
|
|
break
|
|
|
|
# done with logging from the script, so we can collect that thread
|
|
t1 = time.time()
|
|
psr.refresh()
|
|
self._tmp_keyvals['minutes_battery_life_tested'] = (t1 - t0) / 60
|
|
self._tmp_keyvals.update(psr.get_keyvals())
|
|
|
|
|
|
def postprocess_iteration(self):
|
|
"""Postprocess: write keyvals / log and send data to power dashboard."""
|
|
def _log_stats(prefix, stats):
|
|
if not len(stats):
|
|
return
|
|
np = numpy.array(stats)
|
|
logging.debug("%s samples: %d", prefix, len(np))
|
|
logging.debug("%s mean: %.2f", prefix, np.mean())
|
|
logging.debug("%s stdev: %.2f", prefix, np.std())
|
|
logging.debug("%s max: %.2f", prefix, np.max())
|
|
logging.debug("%s min: %.2f", prefix, np.min())
|
|
|
|
|
|
def _log_per_loop_stats():
|
|
samples_per_loop = self._loop_time / self._wait_time + 1
|
|
for kname in self._stats:
|
|
start_idx = 0
|
|
loop = 1
|
|
for end_idx in xrange(samples_per_loop, len(self._stats[kname]),
|
|
samples_per_loop):
|
|
_log_stats("%s loop %d" % (kname, loop),
|
|
self._stats[kname][start_idx:end_idx])
|
|
loop += 1
|
|
start_idx = end_idx
|
|
|
|
|
|
def _log_all_stats():
|
|
for kname in self._stats:
|
|
_log_stats(kname, self._stats[kname])
|
|
|
|
|
|
for task, tstart, tend in self._task_tracker:
|
|
self._checkpoint_logger.checkpoint('_' + task, tstart, tend)
|
|
|
|
keyvals = {}
|
|
for log in self._meas_logs:
|
|
keyvals.update(log.calc())
|
|
keyvals.update(self._statomatic.publish())
|
|
|
|
if self._log_mem_bandwidth:
|
|
self._mlog.stop()
|
|
self._mlog.join()
|
|
|
|
_log_all_stats()
|
|
_log_per_loop_stats()
|
|
|
|
# record battery stats
|
|
if self._power_status.battery:
|
|
keyvals['a_current_now'] = self._power_status.battery.current_now
|
|
keyvals['ah_charge_full'] = \
|
|
self._power_status.battery.charge_full
|
|
keyvals['ah_charge_full_design'] = \
|
|
self._power_status.battery.charge_full_design
|
|
keyvals['ah_charge_start'] = self._ah_charge_start
|
|
keyvals['ah_charge_now'] = self._power_status.battery.charge_now
|
|
keyvals['ah_charge_used'] = keyvals['ah_charge_start'] - \
|
|
keyvals['ah_charge_now']
|
|
keyvals['wh_energy_start'] = self._wh_energy_start
|
|
keyvals['wh_energy_now'] = self._power_status.battery.energy
|
|
keyvals['wh_energy_used'] = keyvals['wh_energy_start'] - \
|
|
keyvals['wh_energy_now']
|
|
keyvals['v_voltage_min_design'] = \
|
|
self._power_status.battery.voltage_min_design
|
|
keyvals['wh_energy_full_design'] = \
|
|
self._power_status.battery.energy_full_design
|
|
keyvals['v_voltage_now'] = self._power_status.battery.voltage_now
|
|
|
|
keyvals.update(self._tmp_keyvals)
|
|
|
|
keyvals['percent_sys_low_battery'] = self._sys_low_batt_p
|
|
keyvals['seconds_sys_low_battery'] = self._sys_low_batt_s
|
|
voltage_np = numpy.array(self._stats['v_voltage_now'])
|
|
voltage_mean = voltage_np.mean()
|
|
keyvals['v_voltage_mean'] = voltage_mean
|
|
|
|
keyvals['wh_energy_powerlogger'] = \
|
|
self._energy_use_from_powerlogger(keyvals)
|
|
|
|
if not self._power_status.on_ac() and keyvals['ah_charge_used'] > 0:
|
|
# For full runs, we should use charge to scale for battery life,
|
|
# since the voltage swing is accounted for.
|
|
# For short runs, energy will be a better estimate.
|
|
if self._loop_count > 1:
|
|
estimated_reps = (keyvals['ah_charge_full_design'] /
|
|
keyvals['ah_charge_used'])
|
|
else:
|
|
estimated_reps = (keyvals['wh_energy_full_design'] /
|
|
keyvals['wh_energy_powerlogger'])
|
|
|
|
bat_life_scale = estimated_reps * \
|
|
((100 - keyvals['percent_sys_low_battery']) / 100)
|
|
|
|
keyvals['minutes_battery_life'] = bat_life_scale * \
|
|
keyvals['minutes_battery_life_tested']
|
|
# In the case where sys_low_batt_s is non-zero subtract those
|
|
# minutes from the final extrapolation.
|
|
if self._sys_low_batt_s:
|
|
keyvals['minutes_battery_life'] -= self._sys_low_batt_s / 60
|
|
|
|
keyvals['a_current_rate'] = keyvals['ah_charge_used'] * 60 / \
|
|
keyvals['minutes_battery_life_tested']
|
|
keyvals['w_energy_rate'] = keyvals['wh_energy_used'] * 60 / \
|
|
keyvals['minutes_battery_life_tested']
|
|
if self._gaia_login:
|
|
self.output_perf_value(description='minutes_battery_life',
|
|
value=keyvals['minutes_battery_life'],
|
|
units='minutes',
|
|
higher_is_better=True)
|
|
|
|
minutes_battery_life_tested = keyvals['minutes_battery_life_tested']
|
|
|
|
# TODO(coconutruben): overwrite write_perf_keyvals for all power
|
|
# tests and replace this once power_LoadTest inherits from power_Test.
|
|
# Dump all keyvals into debug keyvals.
|
|
_utils.write_keyval(os.path.join(self.resultsdir, 'debug_keyval'),
|
|
keyvals)
|
|
# Avoid polluting the keyvals with non-core domains.
|
|
core_keyvals = power_utils.get_core_keyvals(keyvals)
|
|
if not self._gaia_login:
|
|
core_keyvals = {'INVALID_%s' % str(k): v for k, v in
|
|
core_keyvals.iteritems()}
|
|
else:
|
|
for key, value in core_keyvals.iteritems():
|
|
if re.match(r'percent_[cg]pu(idle|pkg).*_R?C0(_C1)?_time', key):
|
|
self.output_perf_value(description=key,
|
|
value=value,
|
|
units='percent',
|
|
higher_is_better=False)
|
|
|
|
self.write_perf_keyval(core_keyvals)
|
|
for log in self._meas_logs:
|
|
log.save_results(self.resultsdir)
|
|
self._checkpoint_logger.save_checkpoint_data(self.resultsdir)
|
|
|
|
if minutes_battery_life_tested * 60 < self._loop_time :
|
|
logging.info('Data is less than 1 loop, skip sending to dashboard.')
|
|
return
|
|
|
|
dashboard_factory = power_dashboard.get_dashboard_factory()
|
|
for log in self._meas_logs:
|
|
dashboard = dashboard_factory.createDashboard(log,
|
|
self.tagged_testname, self.resultsdir, note=self._pdash_note)
|
|
dashboard.upload()
|
|
|
|
|
|
def cleanup(self):
|
|
if self._force_discharge:
|
|
power_utils.charge_control_by_ectool(True)
|
|
if self._backlight:
|
|
self._backlight.restore()
|
|
if self._services:
|
|
self._services.restore_services()
|
|
audio_helper.set_default_volume_levels()
|
|
self._detachable_handler.restore()
|
|
|
|
if self.task_monitor_file:
|
|
self.task_monitor_file.close()
|
|
|
|
if self._shill_proxy:
|
|
if self._force_wifi:
|
|
# cleanup backchannel interface
|
|
# Prevent wifi congestion in test lab by forcing machines to forget the
|
|
# wifi AP we connected to at the start of the test.
|
|
self._shill_proxy.remove_all_wifi_entries()
|
|
|
|
if self._use_cellular_network:
|
|
self._shill_proxy.manager.EnableTechnology(
|
|
shill_proxy.ShillProxy.TECHNOLOGY_WIFI)
|
|
|
|
if self._backchannel:
|
|
self._backchannel.teardown()
|
|
if self._browser:
|
|
self._browser.close()
|
|
if self._testServer:
|
|
self._testServer.stop()
|
|
|
|
|
|
def _do_wait(self, verbose, seconds, latch):
|
|
latched = False
|
|
low_battery = False
|
|
total_time = seconds + self._wait_time
|
|
elapsed_time = 0
|
|
|
|
while elapsed_time < total_time:
|
|
time.sleep(self._wait_time)
|
|
elapsed_time += self._wait_time
|
|
|
|
self._power_status.refresh()
|
|
|
|
if not self._ac_ok and self._power_status.on_ac():
|
|
raise error.TestError('Running on AC power now.')
|
|
|
|
if self._power_status.battery:
|
|
if (not self._ac_ok and
|
|
self._power_status.battery.status != 'Discharging'):
|
|
raise error.TestFail('The battery is not discharging.')
|
|
charge_now = self._power_status.battery.charge_now
|
|
energy_rate = self._power_status.battery.energy_rate
|
|
voltage_now = self._power_status.battery.voltage_now
|
|
self._stats['w_energy_rate'].append(energy_rate)
|
|
self._stats['v_voltage_now'].append(voltage_now)
|
|
if verbose:
|
|
logging.debug('ah_charge_now %f', charge_now)
|
|
logging.debug('w_energy_rate %f', energy_rate)
|
|
logging.debug('v_voltage_now %f', voltage_now)
|
|
|
|
low_battery = (self._power_status.percent_current_charge() <
|
|
self._test_low_batt_p)
|
|
|
|
latched = latch.is_set()
|
|
|
|
if latched or low_battery:
|
|
break
|
|
|
|
if latched:
|
|
# record chrome power extension stats
|
|
form_data = self._testServer.get_form_entries()
|
|
logging.debug(form_data)
|
|
for e in form_data:
|
|
key = 'ext_' + e
|
|
if key in self._tmp_keyvals:
|
|
self._tmp_keyvals[key] += "_%s" % form_data[e]
|
|
else:
|
|
self._tmp_keyvals[key] = form_data[e]
|
|
else:
|
|
logging.debug("Didn't get status back from power extension")
|
|
|
|
return low_battery
|
|
|
|
|
|
def _set_backlight_level(self, loop=None):
|
|
self._backlight.set_default()
|
|
# record brightness level
|
|
self._tmp_keyvals[_loop_keyname(loop, 'level_backlight')] = \
|
|
self._backlight.get_level()
|
|
|
|
|
|
def _set_lightbar_level(self, level='off'):
|
|
"""Set lightbar level.
|
|
|
|
Args:
|
|
level: string value to set lightbar to. See ectool for more details.
|
|
"""
|
|
rv = utils.system('which ectool', ignore_status=True)
|
|
if rv:
|
|
return
|
|
rv = utils.system('ectool lightbar %s' % level, ignore_status=True)
|
|
if rv:
|
|
logging.info('Assuming no lightbar due to non-zero exit status')
|
|
else:
|
|
logging.info('Setting lightbar to %s', level)
|
|
self._tmp_keyvals['level_lightbar_current'] = level
|
|
|
|
|
|
def _has_light_sensor(self):
|
|
"""
|
|
Determine if there is a light sensor on the board.
|
|
|
|
@returns True if this host has a light sensor or
|
|
False if it does not.
|
|
"""
|
|
# If the command exits with a failure status,
|
|
# we do not have a light sensor
|
|
cmd = 'check_powerd_config --ambient_light_sensor'
|
|
result = utils.run(cmd, ignore_status=True)
|
|
if result.exit_status:
|
|
logging.debug('Ambient light sensor not present')
|
|
return False
|
|
logging.debug('Ambient light sensor present')
|
|
return True
|
|
|
|
|
|
def _energy_use_from_powerlogger(self, keyval):
|
|
"""
|
|
Calculates the energy use, in Wh, used over the course of the run as
|
|
reported by the PowerLogger.
|
|
|
|
Args:
|
|
keyval: the dictionary of keyvals containing PowerLogger output
|
|
|
|
Returns:
|
|
energy_wh: total energy used over the course of this run
|
|
|
|
"""
|
|
energy_wh = 0
|
|
loop = 0
|
|
while True:
|
|
duration_key = _loop_keyname(loop, 'system_duration')
|
|
avg_power_key = _loop_keyname(loop, 'system_pwr_avg')
|
|
if duration_key not in keyval or avg_power_key not in keyval:
|
|
break
|
|
energy_wh += keyval[duration_key] * keyval[avg_power_key] / 3600
|
|
loop += 1
|
|
return energy_wh
|
|
|
|
|
|
def _has_hover_detection(self):
|
|
"""
|
|
Checks if hover is detected by the device.
|
|
|
|
Returns:
|
|
Returns True if the hover detection support is enabled.
|
|
Else returns false.
|
|
"""
|
|
|
|
cmd = 'check_powerd_config --hover_detection'
|
|
result = utils.run(cmd, ignore_status=True)
|
|
if result.exit_status:
|
|
logging.debug('Hover not present')
|
|
return False
|
|
logging.debug('Hover present')
|
|
return True
|
|
|
|
|
|
def _set_keyboard_backlight_level(self, loop=None):
|
|
"""
|
|
Sets keyboard backlight based on light sensor and hover.
|
|
These values are based on UMA as mentioned in
|
|
https://bugs.chromium.org/p/chromium/issues/detail?id=603233#c10
|
|
|
|
ALS | hover | keyboard backlight level
|
|
---------------------------------------
|
|
No | No | default
|
|
---------------------------------------
|
|
Yes | No | 40% of default
|
|
--------------------------------------
|
|
No | Yes | System with this configuration does not exist
|
|
--------------------------------------
|
|
Yes | Yes | 30% of default
|
|
--------------------------------------
|
|
|
|
Here default is no Ambient Light Sensor, no hover,
|
|
default always-on brightness level.
|
|
"""
|
|
|
|
default_level = self._keyboard_backlight.get_default_level()
|
|
level_to_set = default_level
|
|
has_light_sensor = self._has_light_sensor()
|
|
has_hover = self._has_hover_detection()
|
|
# TODO(ravisadineni):if (crbug: 603233) becomes default
|
|
# change this to reflect it.
|
|
if has_light_sensor and has_hover:
|
|
level_to_set = (30 * default_level) / 100
|
|
elif has_light_sensor:
|
|
level_to_set = (40 * default_level) / 100
|
|
elif has_hover:
|
|
logging.warn('Device has hover but no light sensor')
|
|
|
|
logging.info('Setting keyboard backlight to %d', level_to_set)
|
|
self._keyboard_backlight.set_level(level_to_set)
|
|
keyname = _loop_keyname(loop, 'percent_kbd_backlight')
|
|
self._tmp_keyvals[keyname] = self._keyboard_backlight.get_percent()
|
|
|
|
|
|
def _log_loop_checkpoint(self, loop, start, end):
|
|
loop_str = _loop_prefix(loop)
|
|
self._checkpoint_logger.checkpoint(loop_str, start, end)
|
|
|
|
# Don't log section if we run custom tasks.
|
|
if self._tasks != '':
|
|
return
|
|
|
|
sections = [
|
|
('browsing', (0, 0.6)),
|
|
('email', (0.6, 0.8)),
|
|
('document', (0.8, 0.9)),
|
|
('video', (0.9, 1)),
|
|
]
|
|
|
|
# Use start time from extension if found by look for google.com start.
|
|
goog_str = loop_str + '_web_page_www.google.com'
|
|
for item, start_extension, _ in self._task_tracker:
|
|
if item == goog_str:
|
|
if start_extension >= start:
|
|
start = start_extension
|
|
break
|
|
logging.warn('Timestamp from extension (%.2f) is earlier than'
|
|
'timestamp from autotest (%.2f).',
|
|
start_extension, start)
|
|
|
|
# Use default loop duration for incomplete loop.
|
|
duration = max(end - start, self._loop_time)
|
|
|
|
for section, fractions in sections:
|
|
s_start, s_end = (start + duration * fraction
|
|
for fraction in fractions)
|
|
if s_start > end:
|
|
break
|
|
if s_end > end:
|
|
s_end = end
|
|
self._checkpoint_logger.checkpoint(section, s_start, s_end)
|
|
loop_section = '_' + loop_str + '_' + section
|
|
self._checkpoint_logger.checkpoint(loop_section, s_start, s_end)
|
|
|
|
|
|
def _extension_task_monitor_handler(self, handler, form):
|
|
"""
|
|
We use the httpd library to allow us to log chrome processes usage.
|
|
"""
|
|
if form:
|
|
logging.debug("[task-monitor] got %d samples", len(form))
|
|
for idx in sorted(form.keys()):
|
|
json = form[idx].value
|
|
self.task_monitor_file.write(json)
|
|
self.task_monitor_file.write(",\n")
|
|
# we don't want to add url information to our keyvals.
|
|
# httpd adds them automatically so we remove them again
|
|
del handler.server._form_entries[idx]
|
|
handler.send_response(200)
|
|
|
|
|
|
def alphanum_key(s):
|
|
""" Turn a string into a list of string and numeric chunks. This enables a
|
|
sort function to use this list as a key to sort alphanumeric strings
|
|
naturally without padding zero digits.
|
|
"z23a" -> ["z", 23, "a"]
|
|
"""
|
|
chunks = re.split('([-.0-9]+)', s)
|
|
for i in range(len(chunks)):
|
|
try:
|
|
chunks[i] = float(chunks[i])
|
|
except ValueError:
|
|
pass
|
|
return chunks
|
|
|
|
|
|
def _extension_log_handler(handler, form, loop_number):
|
|
"""
|
|
We use the httpd library to allow us to log whatever we
|
|
want from the extension JS script into the log files.
|
|
|
|
This method is provided to the server as a handler for
|
|
all requests that come for the log url in the testServer
|
|
|
|
unused parameter, because httpd passes the server itself
|
|
into the handler.
|
|
"""
|
|
|
|
if form:
|
|
for field in sorted(form.keys(), key=alphanum_key):
|
|
logging.debug("[extension] @ %s %s", _loop_prefix(loop_number),
|
|
form[field].value)
|
|
# we don't want to add url information to our keyvals.
|
|
# httpd adds them automatically so we remove them again
|
|
del handler.server._form_entries[field]
|
|
|
|
|
|
def _extension_page_time_info_handler(handler, form, loop_number,
|
|
test_instance):
|
|
page_timestamps = []
|
|
|
|
stats_ids = ['mean', 'min', 'max', 'std']
|
|
loadtime_measurements = []
|
|
sorted_pagelt = []
|
|
#show up to this number of slow page-loads
|
|
num_slow_page_loads = 5
|
|
|
|
if not form:
|
|
logging.debug("no page time information returned")
|
|
return
|
|
|
|
for field in sorted(form.keys(), key=alphanum_key):
|
|
page = json.loads(form[field].value)
|
|
url = page['url']
|
|
|
|
pstr = "[extension] @ %s url: %s" % (_loop_prefix(loop_number), url)
|
|
logging.debug("%s start_time: %d", pstr, page['start_time'])
|
|
|
|
if page['end_load_time']:
|
|
logging.debug("%s end_load_time: %d", pstr, page['end_load_time'])
|
|
|
|
load_time = page['end_load_time'] - page['start_time']
|
|
|
|
loadtime_measurements.append(load_time)
|
|
sorted_pagelt.append((url, load_time))
|
|
|
|
logging.debug("%s load time: %d ms", pstr, load_time)
|
|
|
|
logging.debug("%s end_browse_time: %d", pstr, page['end_browse_time'])
|
|
|
|
page_timestamps.append(page)
|
|
|
|
# we don't want to add url information to our keyvals.
|
|
# httpd adds them automatically so we remove them again
|
|
del handler.server._form_entries[field]
|
|
|
|
page_base = _loop_keyname(loop_number, 'web_page_')
|
|
for page in page_timestamps:
|
|
page_failed = "_failed"
|
|
# timestamps from javascript are in milliseconds, change to seconds
|
|
scale = 1.0/1000
|
|
if page['end_load_time']:
|
|
tagname = page_base + page['url'] + "_load"
|
|
test_instance._task_tracker.append((tagname,
|
|
page['start_time'] * scale, page['end_load_time'] * scale))
|
|
|
|
tagname = page_base + page['url'] + "_browse"
|
|
test_instance._task_tracker.append((tagname,
|
|
page['end_load_time'] * scale, page['end_browse_time'] * scale))
|
|
|
|
page_failed = ""
|
|
|
|
tagname = page_base + page['url'] + page_failed
|
|
test_instance._task_tracker.append((tagname,
|
|
page['start_time'] * scale, page['end_browse_time'] * scale))
|
|
|
|
loadtime_measurements = numpy.array(loadtime_measurements)
|
|
stats_vals = [loadtime_measurements.mean(), loadtime_measurements.min(),
|
|
loadtime_measurements.max(),loadtime_measurements.std()]
|
|
|
|
key_base = 'ext_ms_page_load_time_'
|
|
for i in range(len(stats_ids)):
|
|
key = key_base + stats_ids[i]
|
|
if key in test_instance._tmp_keyvals:
|
|
test_instance._tmp_keyvals[key] += "_%.2f" % stats_vals[i]
|
|
else:
|
|
test_instance._tmp_keyvals[key] = "%.2f" % stats_vals[i]
|
|
|
|
|
|
sorted_pagelt.sort(key=lambda item: item[1], reverse=True)
|
|
|
|
message = "The %d slowest page-load-times are:\n" % (num_slow_page_loads)
|
|
for url, msecs in sorted_pagelt[:num_slow_page_loads]:
|
|
message += "\t%s w/ %d ms" % (url, msecs)
|
|
|
|
logging.debug("%s\n", message)
|
|
|
|
|
|
def _extension_key_values_handler(handler, form, loop_number,
|
|
test_instance):
|
|
if not form:
|
|
logging.debug("no key value information returned")
|
|
return
|
|
|
|
for field in sorted(form.keys(), key=alphanum_key):
|
|
keyval_data = json.loads(form[field].value)
|
|
|
|
# Print each key:value pair and associate it with the data
|
|
for key, value in keyval_data.iteritems():
|
|
logging.debug("[extension] @ %s key: %s val: %s",
|
|
_loop_prefix(loop_number), key, value)
|
|
# Add the key:values to the _tmp_keyvals set
|
|
test_instance._tmp_keyvals[_loop_keyname(loop_number, key)] = value
|
|
|
|
# we don't want to add url information to our keyvals.
|
|
# httpd adds them automatically so we remove them again
|
|
del handler.server._form_entries[field]
|
|
|
|
|
|
def _loop_prefix(loop):
|
|
return "loop%02d" % loop
|
|
|
|
|
|
def _loop_keyname(loop, keyname):
|
|
if loop != None:
|
|
return "%s_%s" % (_loop_prefix(loop), keyname)
|
|
return keyname
|