| # 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, logging, numpy, os, time |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.cros import backchannel, cros_ui, cros_ui_test |
| from autotest_lib.client.cros import httpd, power_status, power_utils |
| from autotest_lib.client.cros import flimflam_test_path |
| from autotest_lib.client.cros.audio import audio_helper |
| import flimflam |
| |
| 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(cros_ui_test.UITest): |
| version = 2 |
| |
| |
| def start_authserver(self): |
| """ |
| Override cros_ui_test.UITest's start_authserver. |
| Do not use auth server and local dns for our test. We need to be |
| able to reach the web. |
| """ |
| pass |
| |
| |
| def ensure_login_complete(self): |
| """ |
| Override cros_ui_test.UITest's ensure_login_complete. |
| Do not use auth server and local dns for our test. We need to be |
| able to reach the web. |
| """ |
| pass |
| |
| |
| def initialize(self, creds='$default', 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='', tasks="", kblight_percent=10, volume_level=10, |
| mic_gain=10): |
| """ |
| percent_initial_charge_min: min battery charge at start of test |
| check_network: check that Ethernet interface is not running |
| loop_count: number of times to loop the test for |
| loop_time: length of time to run the test for in each loop |
| 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 |
| kblight_percent: percent brightness of keyboard backlight |
| volume_level: percent audio volume level |
| mic_gain: percent audio microphone gain level |
| test_low_batt_p: percent battery at which test should stop |
| sys_low_batt_p: percent battery at which power manager will |
| shut-down the device |
| sys_low_batt_s: seconds battery at which power manager will |
| shut-down the device |
| """ |
| self._backlight = 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 = power_status.get_status() |
| self._force_wifi = force_wifi |
| self._testServer = None |
| self._tasks = '\'' + tasks.replace(' ','') + '\'' |
| self._backchannel = None |
| self._kblight_percent = kblight_percent |
| self._volume_level = volume_level |
| self._mic_gain = mic_gain |
| self._wait_time = 60 |
| self._stats = collections.defaultdict(list) |
| |
| 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 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.') |
| |
| if not flimflam.FlimFlam().ConnectService(retries=3, |
| retry=True, |
| service_type='wifi', |
| ssid=wifi_ap, |
| security=wifi_sec, |
| passphrase=wifi_pw, |
| mode='managed')[0]: |
| raise error.TestError('Could not connect to WiFi network.') |
| else: |
| # Find all wired ethernet interfaces. |
| # TODO: combine this with code in network_DisableInterface, in a |
| # common library somewhere. |
| ifaces = [ nic.strip() for nic in os.listdir('/sys/class/net/') |
| if ((not os.path.exists('/sys/class/net/' + nic + '/phy80211')) |
| and nic.find('eth') != -1) ] |
| logging.debug(str(ifaces)) |
| for iface in ifaces: |
| if check_network and \ |
| backchannel.is_network_iface_running(iface): |
| raise error.TestError('Ethernet interface is active. ' + |
| 'Please remove Ethernet cable') |
| |
| self._audio_helper = audio_helper.AudioHelper(None) |
| |
| # record the max backlight level |
| self._backlight = power_utils.Backlight() |
| self._tmp_keyvals['level_backlight_max'] = \ |
| self._backlight.get_max_level() |
| |
| # fix up file perms for the power test extension so that chrome |
| # can access it |
| os.system('chmod -R 755 %s' % self.bindir) |
| |
| # write test parameters to the params.js file to be read by the test |
| # extension. |
| self._write_ext_params() |
| |
| # 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._usb_stats = power_status.USBSuspendStats() |
| self._cpufreq_stats = power_status.CPUFreqStats() |
| self._cpuidle_stats = power_status.CPUIdleStats() |
| self._cpupkg_stats = power_status.CPUPackageStats() |
| self._disk_logger = power_status.DiskStateLogger() |
| if os.path.exists('/dev/sda'): |
| self._disk_logger.start() |
| |
| self._power_status.refresh() |
| (self._sys_low_batt_p, self._sys_low_batt_s) = \ |
| self._get_sys_low_batt_values() |
| if self._sys_low_batt_p and (self._sys_low_batt_p > |
| self._test_low_batt_p): |
| logging.warn("test low battery threshold is below system " + |
| "low battery requirement. Setting to %f", |
| self._sys_low_batt_p) |
| self._test_low_batt_p = self._sys_low_batt_p |
| |
| self._ah_charge_start = self._power_status.battery[0].charge_now |
| self._wh_energy_start = self._power_status.battery[0].energy |
| |
| super(power_LoadTest, self).initialize(creds=creds) |
| |
| def run_once(self): |
| |
| t0 = time.time() |
| ext_path = os.path.join(os.path.dirname(__file__), 'extension.crx') |
| |
| try: |
| kblight = power_utils.KbdBacklight() |
| kblight.set(self._kblight_percent) |
| self._tmp_keyvals['percent_kbd_backlight'] = kblight.get() |
| except power_utils.KbdBacklightException as e: |
| logging.info("Assuming no keyboard backlight due to :: %s", str(e)) |
| kblight = None |
| |
| for i in range(self._loop_count): |
| # the power test extension will report its status here |
| latch = self._testServer.add_wait_url('/status') |
| |
| # Installing the extension will also fire it up. |
| ext_id = self.pyauto.InstallExtension(ext_path) |
| |
| # stop powerd |
| utils.system('stop powerd', ignore_status=True) |
| |
| # reset X settings since X gets restarted upon login |
| self._do_xset() |
| |
| # reset backlight level since powerd might've modified it |
| # based on ambient light |
| self._set_backlight_level() |
| self._set_lightbar_level() |
| if kblight: |
| kblight.set(self._kblight_percent) |
| self._audio_helper.set_volume_levels(self._volume_level, |
| self._mic_gain) |
| |
| low_battery = self._do_wait(self._verbose, self._loop_time, |
| latch) |
| |
| if self._verbose: |
| logging.debug('loop %d completed' % i) |
| |
| if low_battery: |
| logging.info('Exiting due to low battery') |
| break |
| |
| if not self.pyauto.GetBrowserWindowCount(): |
| self.pyauto.OpenNewBrowserWindow(True) |
| self.pyauto.UninstallExtensionById(ext_id) |
| |
| t1 = time.time() |
| self._tmp_keyvals['minutes_battery_life_tested'] = (t1 - t0) / 60 |
| |
| |
| def postprocess_iteration(self): |
| def _format_stats(keyvals, stats, name): |
| for state in stats: |
| keyvals['percent_%s_%s_time' % (name, state)] = stats[state] |
| |
| |
| 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]) |
| |
| |
| keyvals = {} |
| |
| _format_stats(keyvals, self._usb_stats.refresh(), 'usb') |
| _format_stats(keyvals, self._cpufreq_stats.refresh(), 'cpufreq') |
| _format_stats(keyvals, self._cpupkg_stats.refresh(), 'cpupkg') |
| _format_stats(keyvals, self._cpuidle_stats.refresh(), 'cpuidle') |
| |
| _log_all_stats() |
| _log_per_loop_stats() |
| if (self._disk_logger.get_error()): |
| keyvals['disk_logging_error'] = str(self._disk_logger.get_error()) |
| else: |
| _format_stats(keyvals, self._disk_logger.result(), 'disk') |
| |
| # record battery stats |
| keyvals['a_current_now'] = self._power_status.battery[0].current_now |
| keyvals['ah_charge_full'] = self._power_status.battery[0].charge_full |
| keyvals['ah_charge_full_design'] = \ |
| self._power_status.battery[0].charge_full_design |
| keyvals['ah_charge_start'] = self._ah_charge_start |
| keyvals['ah_charge_now'] = self._power_status.battery[0].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[0].energy |
| keyvals['wh_energy_used'] = keyvals['wh_energy_start'] - \ |
| keyvals['wh_energy_now'] |
| keyvals['v_voltage_min_design'] = \ |
| self._power_status.battery[0].voltage_min_design |
| keyvals['wh_energy_full_design'] = \ |
| self._power_status.battery[0].energy_full_design |
| keyvals['v_voltage_now'] = self._power_status.battery[0].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 |
| bat_life_scale = (keyvals['wh_energy_full_design'] / |
| (keyvals['ah_charge_used'] * voltage_mean)) * \ |
| ((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'] |
| keyvals['mc_min_temp'] = self._power_status.min_temp |
| keyvals['mc_max_temp'] = self._power_status.max_temp |
| |
| self.write_perf_keyval(keyvals) |
| |
| |
| def cleanup(self): |
| # re-enable powerd |
| utils.system('start powerd', ignore_status=True) |
| |
| if self._backlight: |
| self._backlight.restore() |
| # cleanup backchannel interface |
| if self._backchannel: |
| self._backchannel.teardown() |
| if self._testServer: |
| self._testServer.stop() |
| super(power_LoadTest, self).cleanup() |
| |
| |
| def _write_ext_params(self): |
| data = '' |
| template = 'var %s = %s;\n' |
| for k in params_dict: |
| data += template % (k, getattr(self, params_dict[k])) |
| |
| filename = os.path.join(self.bindir, 'params.js') |
| utils.open_write_close(filename, data) |
| |
| logging.debug('filename ' + filename) |
| logging.debug(data) |
| |
| |
| 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() |
| charge_now = self._power_status.battery[0].charge_now |
| energy_rate = self._power_status.battery[0].energy_rate |
| voltage_now = self._power_status.battery[0].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] += 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 _do_xset(self): |
| XSET = 'LD_LIBRARY_PATH=/usr/local/lib xset' |
| # Disable X screen saver |
| cros_ui.xsystem('%s s 0 0' % XSET) |
| # Disable DPMS Standby/Suspend/Off |
| cros_ui.xsystem('%s dpms 0 0 0' % XSET) |
| # Force monitor on |
| cros_ui.xsystem('%s dpms force on' % XSET) |
| # Save off X settings |
| cros_ui.xsystem('%s q' % XSET) |
| |
| |
| def _set_backlight_level(self): |
| self._backlight.set_default() |
| # record brightness level |
| self._tmp_keyvals['level_backlight_current'] = \ |
| 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 _get_sys_low_batt_values(self): |
| """Determine the low battery values for device and return. |
| |
| 2012/11/01: power manager (powerd.cc) parses parameters in filesystem |
| and outputs a log message like: |
| |
| [1101/173837:INFO:powerd.cc(258)] Using low battery time threshold |
| of 0 secs and using low battery percent threshold of 3.5 |
| |
| It currently checks to make sure that only one of these values is |
| defined. |
| |
| Returns: |
| Tuple of (percent, seconds) |
| percent: float of low battery percentage |
| seconds: float of low battery seconds |
| |
| """ |
| split_re = 'threshold of' |
| |
| powerd_log = '/var/log/power_manager/powerd.LATEST' |
| cmd = 'grep "low battery time" %s' % powerd_log |
| line = utils.system_output(cmd) |
| secs = float(line.split(split_re)[1].split()[0]) |
| percent = float(line.split(split_re)[2].split()[0]) |
| if secs and percent: |
| raise error.TestError("Low battery percent and seconds " + |
| "are non-zero.") |
| return (percent, secs) |