| # 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 logging |
| import os |
| import time |
| import urllib |
| |
| from autotest_lib.client.bin import test, utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chrome |
| from autotest_lib.client.cros import backchannel |
| from autotest_lib.client.cros import httpd |
| from autotest_lib.client.cros import service_stopper |
| from autotest_lib.client.cros.graphics import graphics_utils |
| from autotest_lib.client.cros.networking import wifi_proxy |
| from autotest_lib.client.cros.power import power_rapl, power_status, power_utils |
| |
| |
| class power_Consumption(test.test): |
| """Measure power consumption for different types of loads. |
| |
| This test runs a series of different tasks like media playback, flash |
| animation, large file download etc. It measures and reports power |
| consumptions during each of those tasks. |
| """ |
| |
| version = 2 |
| |
| |
| def initialize(self, ac_ok=False): |
| """Initialize test. |
| |
| Args: |
| ac_ok: boolean to allow running on AC |
| """ |
| # Objects that need to be taken care of in cleanup() are initialized |
| # here to None. Otherwise we run the risk of AttributeError raised in |
| # cleanup() masking a real error that caused the test to fail during |
| # initialize() before those variables were assigned. |
| self._backlight = None |
| self._tmp_keyvals = {} |
| |
| self._services = service_stopper.ServiceStopper( |
| service_stopper.ServiceStopper.POWER_DRAW_SERVICES) |
| self._services.stop_services() |
| |
| |
| # Time to exclude from calculation after firing a task [seconds] |
| self._stabilization_seconds = 5 |
| self._power_status = power_status.get_status() |
| self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac() |
| |
| if not ac_ok: |
| # Verify that we are running on battery and the battery is |
| # sufficiently charged |
| self._power_status.assert_battery_state(30) |
| |
| # Local data and web server settings. Tarballs with traditional names |
| # like *.tgz don't get copied to the image by ebuilds (see |
| # AUTOTEST_FILE_MASK in autotest-chrome ebuild). |
| self._static_sub_dir = 'static_sites' |
| utils.extract_tarball_to_dir( |
| 'static_sites.tgz.keep', |
| os.path.join(self.bindir, self._static_sub_dir)) |
| self._media_dir = '/home/chronos/user/Downloads/' |
| self._httpd_port = 8000 |
| self._url_base = 'http://localhost:%s/' % self._httpd_port |
| self._test_server = httpd.HTTPListener(self._httpd_port, |
| docroot=self.bindir) |
| |
| # initialize various interesting power related stats |
| self._statomatic = power_status.StatoMatic() |
| self._test_server.run() |
| |
| |
| logging.info('initialize() finished') |
| |
| |
| def _download_test_data(self): |
| """Download audio and video files. |
| |
| This is also used as payload for download test. |
| |
| Note, can reach payload via browser at |
| https://console.developers.google.com/storage/chromeos-test-public/big_buck_bunny |
| Start with README |
| """ |
| |
| repo = 'http://commondatastorage.googleapis.com/chromeos-test-public/' |
| file_list = [repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.mp4', ] |
| if not self.short: |
| file_list += [ |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.ogg', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp8.webm', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp9.webm', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.mp4', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.ogg', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp8.webm', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp9.webm', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.mp4', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.ogg', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp8.webm', |
| repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp9.webm', |
| repo + 'wikimedia/Greensleeves.ogg', |
| ] |
| |
| for url in file_list: |
| logging.info('Downloading %s', url) |
| utils.unmap_url('', url, self._media_dir) |
| |
| |
| def _toggle_fullscreen(self): |
| """Toggle full screen mode.""" |
| # Note: full screen mode toggled with F11 is different from clicking the |
| # full screen icon on video player controls. This needs improvement. |
| # Bug: http://crbug.com/248939 |
| graphics_utils.screen_toggle_fullscreen() |
| |
| |
| # Below are a series of generic sub-test runners. They run a given task |
| # and record the task name and start-end timestamps for future computation |
| # of power consumption during the task. |
| def _run_func(self, name, func, repeat=1, save_checkpoint=True): |
| """Run a given python function as a sub-test.""" |
| start_time = time.time() + self._stabilization_seconds |
| for _ in xrange(repeat): |
| ret = func() |
| if save_checkpoint: |
| self._plog.checkpoint(name, start_time) |
| return ret |
| |
| |
| def _run_sleep(self, name, seconds=60): |
| """Just sleep and record it as a named sub-test""" |
| start_time = time.time() + self._stabilization_seconds |
| time.sleep(seconds) |
| self._plog.checkpoint(name, start_time) |
| |
| |
| def _run_cmd(self, name, cmd, repeat=1): |
| """Run command in a shell as a sub-test""" |
| start_time = time.time() + self._stabilization_seconds |
| for _ in xrange(repeat): |
| logging.info('Executing command: %s', cmd) |
| exit_status = utils.system(cmd, ignore_status=True) |
| if exit_status != 0: |
| logging.error('run_cmd: the following command terminated with' |
| 'a non zero exit status: %s', cmd) |
| self._plog.checkpoint(name, start_time) |
| return exit_status |
| |
| |
| def _run_until(self, name, predicate, timeout=60): |
| """Probe the |predicate| function and wait until it returns true. |
| Record the waiting time as a sub-test |
| """ |
| start_time = time.time() + self._stabilization_seconds |
| utils.poll_for_condition(predicate, timeout=timeout) |
| self._plog.checkpoint(name, start_time) |
| |
| |
| def _run_url(self, name, url, duration): |
| """Navigate to URL, sleep for some time and record it as a sub-test.""" |
| logging.info('Navigating to %s', url) |
| self._tab.Activate() |
| self._tab.Navigate(url) |
| self._run_sleep(name, duration) |
| tab_title = self._tab.EvaluateJavaScript('document.title') |
| logging.info('Sub-test name: %s Tab title: %s.', name, tab_title) |
| |
| |
| def _run_url_bg(self, name, url, duration): |
| """Run a web site in background tab. |
| |
| Navigate to the given URL, open an empty tab to put the one with the |
| URL in background, then sleep and record it as a sub-test. |
| |
| Args: |
| name: sub-test name. |
| url: url to open in background tab. |
| duration: number of seconds to sleep while taking measurements. |
| """ |
| bg_tab = self._tab |
| bg_tab.Navigate(url) |
| # Let it load and settle |
| time.sleep(self._stabilization_seconds / 2.) |
| tab_title = bg_tab.EvaluateJavaScript('document.title') |
| logging.info('App name: %s Tab title: %s.', name, tab_title) |
| # Open a new empty tab to cover the one with test payload. |
| fg_tab = self._browser.tabs.New() |
| fg_tab.Activate() |
| self._run_sleep(name, duration) |
| fg_tab.Close() |
| bg_tab.Activate() |
| |
| |
| def _run_group_download(self): |
| """Download over ethernet. Using video test data as payload.""" |
| |
| # For short run, the payload is too small to take measurement |
| self._run_func('download_eth', |
| self._download_test_data , |
| repeat=self._repeats, |
| save_checkpoint=not(self.short)) |
| |
| |
| def _run_group_webpages(self): |
| """Runs a series of web pages as sub-tests.""" |
| data_url = self._url_base + self._static_sub_dir + '/' |
| |
| # URLs to be only tested in foreground tab. |
| # Can't use about:blank here - crbug.com/248945 |
| # but chrome://version is just as good for our needs. |
| urls = [('ChromeVer', 'chrome://version/')] |
| # URLs to be tested in both, background and foreground modes. |
| bg_urls = [] |
| |
| more_urls = [('BallsDHTML', |
| data_url + 'balls/DHTMLBalls/dhtml.htm'), |
| ('BallsFlex', |
| data_url + 'balls/FlexBalls/flexballs.html'), |
| ] |
| |
| if self.short: |
| urls += more_urls |
| else: |
| bg_urls += more_urls |
| bg_urls += [('Parapluesch', |
| 'http://www.parapluesch.de/whiskystore/test.htm'), |
| ('PosterCircle', |
| 'http://www.webkit.org' |
| '/blog-files/3d-transforms/poster-circle.html'), ] |
| |
| for name, url in urls + bg_urls: |
| self._run_url(name, url, duration=self._duration_secs) |
| |
| for name, url in bg_urls: |
| self._run_url_bg('bg_' + name, url, duration=self._duration_secs) |
| |
| |
| def _run_group_speedometer(self): |
| """Run the Speedometer benchmark suite as a sub-test. |
| |
| Fire it up and wait until it displays "Score". |
| """ |
| |
| # TODO: check in a local copy of the test if we can get permission if |
| # the network causes problems. |
| url = 'http://browserbench.org/Speedometer/' |
| start_js = 'startTest()' |
| score_js = "document.getElementById('result-number').innerText" |
| tab = self._tab |
| |
| def speedometer_func(): |
| """To be passed as the callable to self._run_func()""" |
| tab.Navigate(url) |
| tab.WaitForDocumentReadyStateToBeComplete() |
| tab.EvaluateJavaScript(start_js) |
| # Speedometer test should be done in less than 15 minutes (actual |
| # runs are closer to 5). |
| is_done = lambda: tab.EvaluateJavaScript(score_js) != "" |
| time.sleep(self._stabilization_seconds) |
| utils.poll_for_condition(is_done, timeout=900, |
| desc='Speedometer score found') |
| |
| self._run_func('Speedometer', speedometer_func, repeat=self._repeats) |
| |
| # Write speedometer score from the last run to log |
| score = tab.EvaluateJavaScript(score_js) |
| logging.info('Speedometer Score: %s', score) |
| |
| |
| def _run_group_video(self): |
| """Run video and audio playback in the browser.""" |
| |
| # Note: for perf keyvals, key names are defined as VARCHAR(30) in the |
| # results DB. Chars above 30 are truncated when saved to DB. |
| urls = [('vid400p_h264', 'big_buck_bunny_trailer_400p.mp4'), ] |
| fullscreen_urls = [] |
| bg_urls = [] |
| |
| if not self.short: |
| urls += [ |
| ('vid400p_ogg', 'big_buck_bunny_trailer_400p.ogg'), |
| ('vid400p_vp8', 'big_buck_bunny_trailer_400p.vp8.webm'), |
| ('vid400p_vp9', 'big_buck_bunny_trailer_400p.vp9.webm'), |
| ('vid720_h264', 'big_buck_bunny_trailer_720p.mp4'), |
| ('vid720_ogg', 'big_buck_bunny_trailer_720p.ogg'), |
| ('vid720_vp8', 'big_buck_bunny_trailer_720p.vp8.webm'), |
| ('vid720_vp9', 'big_buck_bunny_trailer_720p.vp9.webm'), |
| ('vid1080_h264', 'big_buck_bunny_trailer_1080p.mp4'), |
| ('vid1080_ogg', 'big_buck_bunny_trailer_1080p.ogg'), |
| ('vid1080_vp8', 'big_buck_bunny_trailer_1080p.vp8.webm'), |
| ('vid1080_vp9', 'big_buck_bunny_trailer_1080p.vp9.webm'), |
| ('audio', 'Greensleeves.ogg'), |
| ] |
| |
| fullscreen_urls += [ |
| ('vid720_h264_fs', 'big_buck_bunny_trailer_720p.mp4'), |
| ('vid720_vp8_fs', 'big_buck_bunny_trailer_720p.vp8.webm'), |
| ('vid720_vp9_fs', 'big_buck_bunny_trailer_720p.vp9.webm'), |
| ('vid1080_h264_fs', 'big_buck_bunny_trailer_1080p.mp4'), |
| ('vid1080_vp8_fs', 'big_buck_bunny_trailer_1080p.vp8.webm'), |
| ('vid1080_vp9_fs', 'big_buck_bunny_trailer_1080p.vp9.webm'), |
| ] |
| |
| bg_urls += [ |
| ('bg_vid400p', 'big_buck_bunny_trailer_400p.vp8.webm'), |
| ] |
| |
| # The video files are run from a file:// url. In order to work properly |
| # from an http:// url, some careful web server configuration is needed |
| def full_url(filename): |
| """Create a file:// url for the media file and verify it exists. |
| |
| @param filename: string |
| """ |
| p = os.path.join(self._media_dir, filename) |
| if not os.path.isfile(p): |
| raise error.TestError('Media file %s is missing.', p) |
| return 'file://' + p |
| |
| js_loop_enable = """ve = document.getElementsByTagName('video')[0]; |
| ve.loop = true; |
| ve.play(); |
| """ |
| |
| for name, url in urls: |
| logging.info('Playing video %s', url) |
| self._tab.Navigate(full_url(url)) |
| self._tab.ExecuteJavaScript(js_loop_enable) |
| self._run_sleep(name, self._duration_secs) |
| |
| for name, url in fullscreen_urls: |
| self._toggle_fullscreen() |
| self._tab.Navigate(full_url(url)) |
| self._tab.ExecuteJavaScript(js_loop_enable) |
| self._run_sleep(name, self._duration_secs) |
| self._toggle_fullscreen() |
| |
| for name, url in bg_urls: |
| logging.info('Playing video in background tab %s', url) |
| self._tab.Navigate(full_url(url)) |
| self._tab.ExecuteJavaScript(js_loop_enable) |
| fg_tab = self._browser.tabs.New() |
| self._run_sleep(name, self._duration_secs) |
| fg_tab.Close() |
| self._tab.Activate() |
| |
| |
| def _run_group_sound(self): |
| """Run non-UI sound test using 'speaker-test'.""" |
| # For some reason speaker-test won't work on CrOS without a reasonable |
| # buffer size specified with -b. |
| # http://crbug.com/248955 |
| cmd = 'speaker-test -l %s -t sine -c 2 -b 16384' % (self._repeats * 6) |
| self._run_cmd('speaker_test', cmd) |
| |
| |
| def _run_group_lowlevel(self): |
| """Low level system stuff""" |
| mb = min(1024, 32 * self._repeats) |
| self._run_cmd('memtester', '/usr/local/sbin/memtester %s 1' % mb) |
| |
| # one rep of dd takes about 15 seconds |
| root_dev = utils.get_root_partition() |
| cmd = 'dd if=%s of=/dev/null' % root_dev |
| self._run_cmd('dd', cmd, repeat=2 * self._repeats) |
| |
| |
| def _run_group_backchannel(self): |
| """WiFi sub-tests.""" |
| |
| shill = wifi_proxy.WifiProxy() |
| for _ in xrange(3): |
| succeeded, _, _, _, _ = shill.connect_to_wifi_network( |
| ssid='GoogleGuest', |
| security='none', |
| security_parameters={}, |
| save_credentials=False) |
| if succeeded: |
| break |
| |
| if not succeeded: |
| logging.error("Could not connect to WiFi") |
| return |
| |
| logging.info('Starting Backchannel') |
| with backchannel.Backchannel(): |
| # Wifi needs some time to recover after backchanel is activated |
| # TODO (kamrik) remove this sleep, once backchannel handles this |
| time.sleep(15) |
| |
| cmd = 'ping -c %s www.google.com' % (self._duration_secs) |
| self._run_cmd('ping_wifi', cmd) |
| |
| # This URL must be visible from WiFi network used for test |
| big_file_url = ('http://googleappengine.googlecode.com' |
| '/files/GoogleAppEngine-1.6.2.msi') |
| cmd = 'curl %s > /dev/null' % big_file_url |
| self._run_cmd('download_wifi', cmd, repeat=self._repeats) |
| |
| |
| def _run_group_backlight(self): |
| """Vary backlight brightness and record power at each setting.""" |
| for i in [100, 50, 0]: |
| self._backlight.set_percent(i) |
| start_time = time.time() + self._stabilization_seconds |
| time.sleep(30 * self._repeats) |
| self._plog.checkpoint('backlight_%03d' % i, start_time) |
| self._backlight.set_default() |
| |
| |
| def _web_echo(self, msg): |
| """ Displays a message in the browser.""" |
| url = self._url_base + 'echo.html?' |
| url += urllib.quote(msg) |
| self._tab.Navigate(url) |
| |
| |
| def _run_test_groups(self, groups): |
| """ Run all the test groups. |
| |
| Args: |
| groups: list of sub-test groups to run. Each sub-test group refers |
| to a _run_group_...() function. |
| """ |
| |
| for group in groups: |
| logging.info('Running group %s', group) |
| # The _web_echo here is important for some tests (esp. non UI) |
| # it gets the previous web page replaced with an almost empty one. |
| self._tab.Activate() |
| self._web_echo('Running test %s' % group) |
| test_func = getattr(self, '_run_group_%s' % group) |
| test_func() |
| |
| |
| def run_once(self, short=False, test_groups=None, reps=1): |
| # Some sub-tests have duration specified directly, _base_secs * reps |
| # is used in this case. Others complete whenever the underlying task |
| # completes, those are manually tuned to be roughly around |
| # reps * 30 seconds. Don't change _base_secs unless you also |
| # change the manual tuning in sub-tests |
| self._base_secs = 30 |
| self._repeats = reps |
| self._duration_secs = self._base_secs * reps |
| |
| # Lists of default tests to run |
| UI_TESTS = ['backlight', 'download', 'webpages', 'video', 'speedometer'] |
| NONUI_TESTS = ['backchannel', 'sound', 'lowlevel'] |
| DEFAULT_TESTS = UI_TESTS + NONUI_TESTS |
| DEFAULT_SHORT_TESTS = ['download', 'webpages', 'video'] |
| |
| self.short = short |
| if test_groups is None: |
| if self.short: |
| test_groups = DEFAULT_SHORT_TESTS |
| else: |
| test_groups = DEFAULT_TESTS |
| logging.info('Test groups to run: %s', ', '.join(test_groups)) |
| |
| self._backlight = power_utils.Backlight() |
| self._backlight.set_default() |
| |
| measure = [] |
| if not self._power_status.on_ac(): |
| measure += \ |
| [power_status.SystemPower(self._power_status.battery_path)] |
| if power_utils.has_powercap_support(): |
| measure += power_rapl.create_powercap() |
| elif power_utils.has_rapl_support(): |
| measure += power_rapl.create_rapl() |
| self._plog = power_status.PowerLogger(measure) |
| self._plog.start() |
| |
| # Log in. |
| with chrome.Chrome() as cr: |
| self._browser = cr.browser |
| graphics_utils.screen_disable_energy_saving() |
| # Most of the tests will be running in this tab. |
| self._tab = cr.browser.tabs[0] |
| |
| # Verify that we have a functioning browser and local web server. |
| self._tab.Activate() |
| self._web_echo("Sanity_test") |
| self._tab.WaitForDocumentReadyStateToBeComplete() |
| |
| # Video test must have the data from download test |
| if ('video' in test_groups): |
| iv = test_groups.index('video') |
| if 'download' not in test_groups[:iv]: |
| msg = '"download" test must run before "video".' |
| raise error.TestError(msg) |
| |
| # Run all the test groups |
| self._run_test_groups(test_groups) |
| |
| # Wrap up |
| keyvals = self._plog.calc() |
| keyvals.update(self._tmp_keyvals) |
| keyvals.update(self._statomatic.publish()) |
| |
| # check AC status is still the same as init |
| self._power_status.refresh() |
| on_ac = self._power_status.on_ac() |
| if keyvals['b_on_ac'] != on_ac: |
| raise error.TestError('on AC changed between start & stop of test') |
| |
| if not on_ac: |
| whrs = self._power_status.battery[0].energy_full_design |
| logging.info("energy_full_design = %0.3f Wh", whrs) |
| |
| # Calculate expected battery life time with ChromeVer power draw |
| idle_name = 'ChromeVer_system_pwr_avg' |
| if idle_name in keyvals: |
| hours_life = whrs / keyvals[idle_name] |
| keyvals['hours_battery_ChromeVer'] = hours_life |
| |
| # Calculate a weighted power draw and battery life time. The weights |
| # are intended to represent "typical" usage. Some video, some Flash |
| # ... and most of the time idle. see, |
| # http://www.chromium.org/chromium-os/testing/power-testing |
| weights = {'vid400p_h264_system_pwr_avg':0.1, |
| 'BallsFlex_system_pwr_avg':0.1, |
| 'BallsDHTML_system_pwr_avg':0.3, |
| } |
| weights[idle_name] = 1 - sum(weights.values()) |
| |
| if set(weights).issubset(set(keyvals)): |
| p = sum(w * keyvals[k] for (k, w) in weights.items()) |
| keyvals['w_Weighted_system_pwr_avg'] = p |
| keyvals['hours_battery_Weighted'] = whrs / p |
| |
| self.write_perf_keyval(keyvals) |
| self._plog.save_results(self.resultsdir) |
| |
| |
| def cleanup(self): |
| # cleanup() is run by common_lib/test.py |
| try: |
| self._test_server.stop() |
| except AttributeError: |
| logging.debug('test_server could not be stopped in cleanup') |
| |
| if self._backlight: |
| self._backlight.restore() |
| if self._services: |
| self._services.restore_services() |
| |
| super(power_Consumption, self).cleanup() |