Home | History | Annotate | Download | only in power_Consumption
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 import os
      7 import time
      8 import urllib
      9 
     10 from autotest_lib.client.bin import test, utils
     11 from autotest_lib.client.common_lib import error
     12 from autotest_lib.client.common_lib.cros import chrome
     13 from autotest_lib.client.cros import backchannel
     14 # pylint: disable=W0611
     15 from autotest_lib.client.cros import flimflam_test_path  # Needed for flimflam
     16 from autotest_lib.client.cros import httpd
     17 from autotest_lib.client.cros import power_rapl, power_status, power_utils
     18 from autotest_lib.client.cros import service_stopper
     19 from autotest_lib.client.cros.graphics import graphics_utils
     20 import flimflam  # Requires flimflam_test_path to be imported first.
     21 
     22 
     23 class power_Consumption(test.test):
     24     """Measure power consumption for different types of loads.
     25 
     26     This test runs a series of different tasks like media playback, flash
     27     animation, large file download etc. It measures and reports power
     28     consumptions during each of those tasks.
     29     """
     30 
     31     version = 2
     32 
     33 
     34     def initialize(self, ac_ok=False):
     35         """Initialize test.
     36 
     37         Args:
     38             ac_ok: boolean to allow running on AC
     39         """
     40         # Objects that need to be taken care of in cleanup() are initialized
     41         # here to None. Otherwise we run the risk of AttributeError raised in
     42         # cleanup() masking a real error that caused the test to fail during
     43         # initialize() before those variables were assigned.
     44         self._backlight = None
     45         self._tmp_keyvals = {}
     46 
     47         self._services = service_stopper.ServiceStopper(
     48             service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
     49         self._services.stop_services()
     50 
     51 
     52         # Time to exclude from calculation after firing a task [seconds]
     53         self._stabilization_seconds = 5
     54         self._power_status = power_status.get_status()
     55         self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac()
     56 
     57         if not ac_ok:
     58             # Verify that we are running on battery and the battery is
     59             # sufficiently charged
     60             self._power_status.assert_battery_state(30)
     61 
     62         # Local data and web server settings. Tarballs with traditional names
     63         # like *.tgz don't get copied to the image by ebuilds (see
     64         # AUTOTEST_FILE_MASK in autotest-chrome ebuild).
     65         self._static_sub_dir = 'static_sites'
     66         utils.extract_tarball_to_dir(
     67                 'static_sites.tgz.keep',
     68                 os.path.join(self.bindir, self._static_sub_dir))
     69         self._media_dir = '/home/chronos/user/Downloads/'
     70         self._httpd_port = 8000
     71         self._url_base = 'http://localhost:%s/' % self._httpd_port
     72         self._test_server = httpd.HTTPListener(self._httpd_port,
     73                                                docroot=self.bindir)
     74 
     75         # initialize various interesting power related stats
     76         self._statomatic = power_status.StatoMatic()
     77         self._test_server.run()
     78 
     79 
     80         logging.info('initialize() finished')
     81 
     82 
     83     def _download_test_data(self):
     84         """Download audio and video files.
     85 
     86         This is also used as payload for download test.
     87 
     88         Note, can reach payload via browser at
     89           https://console.developers.google.com/storage/chromeos-test-public/big_buck_bunny
     90         Start with README
     91         """
     92 
     93         repo = 'http://commondatastorage.googleapis.com/chromeos-test-public/'
     94         file_list = [repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.mp4', ]
     95         if not self.short:
     96             file_list += [
     97                 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.ogg',
     98                 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp8.webm',
     99                 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp9.webm',
    100                 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.mp4',
    101                 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.ogg',
    102                 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp8.webm',
    103                 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp9.webm',
    104                 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.mp4',
    105                 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.ogg',
    106                 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp8.webm',
    107                 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp9.webm',
    108                 repo + 'wikimedia/Greensleeves.ogg',
    109                 ]
    110 
    111         for url in file_list:
    112             logging.info('Downloading %s', url)
    113             utils.unmap_url('', url, self._media_dir)
    114 
    115 
    116     def _toggle_fullscreen(self):
    117         """Toggle full screen mode."""
    118         # Note: full screen mode toggled with F11 is different from clicking the
    119         # full screen icon on video player controls. This needs improvement.
    120         # Bug: http://crbug.com/248939
    121         graphics_utils.screen_toggle_fullscreen()
    122 
    123 
    124     # Below are a series of generic sub-test runners. They run a given task
    125     # and record the task name and start-end timestamps for future computation
    126     # of power consumption during the task.
    127     def _run_func(self, name, func, repeat=1, save_checkpoint=True):
    128         """Run a given python function as a sub-test."""
    129         start_time = time.time() + self._stabilization_seconds
    130         for _ in xrange(repeat):
    131             ret = func()
    132         if save_checkpoint:
    133             self._plog.checkpoint(name, start_time)
    134         return ret
    135 
    136 
    137     def _run_sleep(self, name, seconds=60):
    138         """Just sleep and record it as a named sub-test"""
    139         start_time = time.time() + self._stabilization_seconds
    140         time.sleep(seconds)
    141         self._plog.checkpoint(name, start_time)
    142 
    143 
    144     def _run_cmd(self, name, cmd, repeat=1):
    145         """Run command in a shell as a sub-test"""
    146         start_time = time.time() + self._stabilization_seconds
    147         for _ in xrange(repeat):
    148             logging.info('Executing command: %s', cmd)
    149             exit_status = utils.system(cmd, ignore_status=True)
    150             if exit_status != 0:
    151                 logging.error('run_cmd: the following command terminated with'
    152                                 'a non zero exit status: %s', cmd)
    153         self._plog.checkpoint(name, start_time)
    154         return exit_status
    155 
    156 
    157     def _run_until(self, name, predicate, timeout=60):
    158         """Probe the |predicate| function  and wait until it returns true.
    159         Record the waiting time as a sub-test
    160         """
    161         start_time = time.time() + self._stabilization_seconds
    162         utils.poll_for_condition(predicate, timeout=timeout)
    163         self._plog.checkpoint(name, start_time)
    164 
    165 
    166     def _run_url(self, name, url, duration):
    167         """Navigate to URL, sleep for some time and record it as a sub-test."""
    168         logging.info('Navigating to %s', url)
    169         self._tab.Activate()
    170         self._tab.Navigate(url)
    171         self._run_sleep(name, duration)
    172         tab_title = self._tab.EvaluateJavaScript('document.title')
    173         logging.info('Sub-test name: %s Tab title: %s.', name, tab_title)
    174 
    175 
    176     def _run_url_bg(self, name, url, duration):
    177         """Run a web site in background tab.
    178 
    179         Navigate to the given URL, open an empty tab to put the one with the
    180         URL in background, then sleep and record it as a sub-test.
    181 
    182         Args:
    183             name: sub-test name.
    184             url: url to open in background tab.
    185             duration: number of seconds to sleep while taking measurements.
    186         """
    187         bg_tab = self._tab
    188         bg_tab.Navigate(url)
    189         # Let it load and settle
    190         time.sleep(self._stabilization_seconds / 2.)
    191         tab_title = bg_tab.EvaluateJavaScript('document.title')
    192         logging.info('App name: %s Tab title: %s.', name, tab_title)
    193         # Open a new empty tab to cover the one with test payload.
    194         fg_tab = self._browser.tabs.New()
    195         fg_tab.Activate()
    196         self._run_sleep(name, duration)
    197         fg_tab.Close()
    198         bg_tab.Activate()
    199 
    200 
    201     def _run_group_download(self):
    202         """Download over ethernet. Using video test data as payload."""
    203 
    204         # For short run, the payload is too small to take measurement
    205         self._run_func('download_eth',
    206                        self._download_test_data ,
    207                        repeat=self._repeats,
    208                        save_checkpoint=not(self.short))
    209 
    210 
    211     def _run_group_webpages(self):
    212         """Runs a series of web pages as sub-tests."""
    213         data_url = self._url_base + self._static_sub_dir + '/'
    214 
    215         # URLs to be only tested in foreground tab.
    216         # Can't use about:blank here - crbug.com/248945
    217         # but chrome://version is just as good for our needs.
    218         urls = [('ChromeVer', 'chrome://version/')]
    219         # URLs to be tested in both, background and foreground modes.
    220         bg_urls = []
    221 
    222         more_urls = [('BallsDHTML',
    223                       data_url + 'balls/DHTMLBalls/dhtml.htm'),
    224                      ('BallsFlex',
    225                       data_url + 'balls/FlexBalls/flexballs.html'),
    226                     ]
    227 
    228         if self.short:
    229             urls += more_urls
    230         else:
    231             bg_urls += more_urls
    232             bg_urls += [('Parapluesch',
    233                          'http://www.parapluesch.de/whiskystore/test.htm'),
    234                          ('PosterCircle',
    235                           'http://www.webkit.org'
    236                           '/blog-files/3d-transforms/poster-circle.html'), ]
    237 
    238         for name, url in urls + bg_urls:
    239             self._run_url(name, url, duration=self._duration_secs)
    240 
    241         for name, url in bg_urls:
    242             self._run_url_bg('bg_' + name, url, duration=self._duration_secs)
    243 
    244 
    245     def _run_group_speedometer(self):
    246         """Run the Speedometer benchmark suite as a sub-test.
    247 
    248         Fire it up and wait until it displays "Score".
    249         """
    250 
    251         # TODO: check in a local copy of the test if we can get permission if
    252         # the network causes problems.
    253         url = 'http://browserbench.org/Speedometer/'
    254         start_js = 'startTest()'
    255         score_js = "document.getElementById('result-number').innerText"
    256         tab = self._tab
    257 
    258         def speedometer_func():
    259             """To be passed as the callable to self._run_func()"""
    260             tab.Navigate(url)
    261             tab.WaitForDocumentReadyStateToBeComplete()
    262             tab.EvaluateJavaScript(start_js)
    263             # Speedometer test should be done in less than 15 minutes (actual
    264             # runs are closer to 5).
    265             is_done = lambda: tab.EvaluateJavaScript(score_js) != ""
    266             time.sleep(self._stabilization_seconds)
    267             utils.poll_for_condition(is_done, timeout=900,
    268                                      desc='Speedometer score found')
    269 
    270         self._run_func('Speedometer', speedometer_func, repeat=self._repeats)
    271 
    272         # Write speedometer score from the last run to log
    273         score = tab.EvaluateJavaScript(score_js)
    274         logging.info('Speedometer Score: %s', score)
    275 
    276 
    277     def _run_group_video(self):
    278         """Run video and audio playback in the browser."""
    279 
    280         # Note: for perf keyvals, key names are defined as VARCHAR(30) in the
    281         # results DB. Chars above 30 are truncated when saved to DB.
    282         urls = [('vid400p_h264', 'big_buck_bunny_trailer_400p.mp4'), ]
    283         fullscreen_urls = []
    284         bg_urls = []
    285 
    286         if not self.short:
    287             urls += [
    288                 ('vid400p_ogg', 'big_buck_bunny_trailer_400p.ogg'),
    289                 ('vid400p_vp8', 'big_buck_bunny_trailer_400p.vp8.webm'),
    290                 ('vid400p_vp9', 'big_buck_bunny_trailer_400p.vp9.webm'),
    291                 ('vid720_h264', 'big_buck_bunny_trailer_720p.mp4'),
    292                 ('vid720_ogg', 'big_buck_bunny_trailer_720p.ogg'),
    293                 ('vid720_vp8', 'big_buck_bunny_trailer_720p.vp8.webm'),
    294                 ('vid720_vp9', 'big_buck_bunny_trailer_720p.vp9.webm'),
    295                 ('vid1080_h264', 'big_buck_bunny_trailer_1080p.mp4'),
    296                 ('vid1080_ogg', 'big_buck_bunny_trailer_1080p.ogg'),
    297                 ('vid1080_vp8', 'big_buck_bunny_trailer_1080p.vp8.webm'),
    298                 ('vid1080_vp9', 'big_buck_bunny_trailer_1080p.vp9.webm'),
    299                 ('audio', 'Greensleeves.ogg'),
    300                 ]
    301 
    302             fullscreen_urls += [
    303                 ('vid720_h264_fs', 'big_buck_bunny_trailer_720p.mp4'),
    304                 ('vid720_vp8_fs', 'big_buck_bunny_trailer_720p.vp8.webm'),
    305                 ('vid720_vp9_fs', 'big_buck_bunny_trailer_720p.vp9.webm'),
    306                 ('vid1080_h264_fs', 'big_buck_bunny_trailer_1080p.mp4'),
    307                 ('vid1080_vp8_fs', 'big_buck_bunny_trailer_1080p.vp8.webm'),
    308                 ('vid1080_vp9_fs', 'big_buck_bunny_trailer_1080p.vp9.webm'),
    309                 ]
    310 
    311             bg_urls += [
    312                 ('bg_vid400p', 'big_buck_bunny_trailer_400p.vp8.webm'),
    313                 ]
    314 
    315         # The video files are run from a file:// url. In order to work properly
    316         # from an http:// url, some careful web server configuration is needed
    317         def full_url(filename):
    318             """Create a file:// url for the media file and verify it exists.
    319 
    320             @param filename: string
    321             """
    322             p = os.path.join(self._media_dir, filename)
    323             if not os.path.isfile(p):
    324                 raise error.TestError('Media file %s is missing.', p)
    325             return 'file://' + p
    326 
    327         js_loop_enable = """ve = document.getElementsByTagName('video')[0];
    328                          ve.loop = true;
    329                          ve.play();
    330                          """
    331 
    332         for name, url in urls:
    333             logging.info('Playing video %s', url)
    334             self._tab.Navigate(full_url(url))
    335             self._tab.ExecuteJavaScript(js_loop_enable)
    336             self._run_sleep(name, self._duration_secs)
    337 
    338         for name, url in fullscreen_urls:
    339             self._toggle_fullscreen()
    340             self._tab.Navigate(full_url(url))
    341             self._tab.ExecuteJavaScript(js_loop_enable)
    342             self._run_sleep(name, self._duration_secs)
    343             self._toggle_fullscreen()
    344 
    345         for name, url in bg_urls:
    346             logging.info('Playing video in background tab %s', url)
    347             self._tab.Navigate(full_url(url))
    348             self._tab.ExecuteJavaScript(js_loop_enable)
    349             fg_tab = self._browser.tabs.New()
    350             self._run_sleep(name, self._duration_secs)
    351             fg_tab.Close()
    352             self._tab.Activate()
    353 
    354 
    355     def _run_group_sound(self):
    356         """Run non-UI sound test using 'speaker-test'."""
    357         # For some reason speaker-test won't work on CrOS without a reasonable
    358         # buffer size specified with -b.
    359         # http://crbug.com/248955
    360         cmd = 'speaker-test -l %s -t sine -c 2 -b 16384' % (self._repeats * 6)
    361         self._run_cmd('speaker_test', cmd)
    362 
    363 
    364     def _run_group_lowlevel(self):
    365         """Low level system stuff"""
    366         mb = min(1024, 32 * self._repeats)
    367         self._run_cmd('memtester', '/usr/local/sbin/memtester %s 1' % mb)
    368 
    369         # one rep of dd takes about 15 seconds
    370         root_dev = utils.get_root_partition()
    371         cmd = 'dd if=%s of=/dev/null' % root_dev
    372         self._run_cmd('dd', cmd, repeat=2 * self._repeats)
    373 
    374 
    375     def _run_group_backchannel(self):
    376         """WiFi sub-tests."""
    377 
    378         wifi_ap = 'GoogleGuest'
    379         wifi_sec = 'none'
    380         wifi_pw = ''
    381 
    382         flim = flimflam.FlimFlam()
    383         conn = flim.ConnectService(retries=3,
    384                               retry=True,
    385                               service_type='wifi',
    386                               ssid=wifi_ap,
    387                               security=wifi_sec,
    388                               passphrase=wifi_pw,
    389                               mode='managed')
    390         if not conn[0]:
    391             logging.error("Could not connect to WiFi")
    392             return
    393 
    394         logging.info('Starting Backchannel')
    395         with backchannel.Backchannel():
    396             # Wifi needs some time to recover after backchanel is activated
    397             # TODO (kamrik) remove this sleep, once backchannel handles this
    398             time.sleep(15)
    399 
    400             cmd = 'ping -c %s www.google.com' % (self._duration_secs)
    401             self._run_cmd('ping_wifi', cmd)
    402 
    403             # This URL must be visible from WiFi network used for test
    404             big_file_url = ('http://googleappengine.googlecode.com'
    405                             '/files/GoogleAppEngine-1.6.2.msi')
    406             cmd = 'curl %s > /dev/null' % big_file_url
    407             self._run_cmd('download_wifi', cmd, repeat=self._repeats)
    408 
    409 
    410     def _run_group_backlight(self):
    411         """Vary backlight brightness and record power at each setting."""
    412         for i in [100, 50, 0]:
    413             self._backlight.set_percent(i)
    414             start_time = time.time() + self._stabilization_seconds
    415             time.sleep(30 * self._repeats)
    416             self._plog.checkpoint('backlight_%03d' % i, start_time)
    417         self._backlight.set_default()
    418 
    419 
    420     def _web_echo(self, msg):
    421         """ Displays a message in the browser."""
    422         url = self._url_base + 'echo.html?'
    423         url += urllib.quote(msg)
    424         self._tab.Navigate(url)
    425 
    426 
    427     def _run_test_groups(self, groups):
    428         """ Run all the test groups.
    429 
    430         Args:
    431             groups: list of sub-test groups to run. Each sub-test group refers
    432                 to a _run_group_...() function.
    433         """
    434 
    435         for group in groups:
    436             logging.info('Running group %s', group)
    437             # The _web_echo here is important for some tests (esp. non UI)
    438             # it gets the previous web page replaced with an almost empty one.
    439             self._tab.Activate()
    440             self._web_echo('Running test %s' % group)
    441             test_func = getattr(self, '_run_group_%s' % group)
    442             test_func()
    443 
    444 
    445     def run_once(self, short=False, test_groups=None, reps=1):
    446         # Some sub-tests have duration specified directly, _base_secs * reps
    447         # is used in this case. Others complete whenever the underlying task
    448         # completes, those are manually tuned to be roughly around
    449         # reps * 30 seconds. Don't change _base_secs unless you also
    450         # change the manual tuning in sub-tests
    451         self._base_secs = 30
    452         self._repeats = reps
    453         self._duration_secs = self._base_secs * reps
    454 
    455         # Lists of default tests to run
    456         UI_TESTS = ['backlight', 'download', 'webpages', 'video', 'speedometer']
    457         NONUI_TESTS = ['backchannel', 'sound', 'lowlevel']
    458         DEFAULT_TESTS = UI_TESTS + NONUI_TESTS
    459         DEFAULT_SHORT_TESTS = ['download', 'webpages', 'video']
    460 
    461         self.short = short
    462         if test_groups is None:
    463             if self.short:
    464                 test_groups = DEFAULT_SHORT_TESTS
    465             else:
    466                 test_groups = DEFAULT_TESTS
    467         logging.info('Test groups to run: %s', ', '.join(test_groups))
    468 
    469         self._backlight = power_utils.Backlight()
    470         self._backlight.set_default()
    471 
    472         measure = []
    473         if not self._power_status.on_ac():
    474             measure += \
    475                 [power_status.SystemPower(self._power_status.battery_path)]
    476         if power_utils.has_rapl_support():
    477             measure += power_rapl.create_rapl()
    478         self._plog = power_status.PowerLogger(measure)
    479         self._plog.start()
    480 
    481         # Log in.
    482         with chrome.Chrome() as cr:
    483             self._browser = cr.browser
    484             graphics_utils.screen_disable_energy_saving()
    485             # Most of the tests will be running in this tab.
    486             self._tab = cr.browser.tabs[0]
    487 
    488             # Verify that we have a functioning browser and local web server.
    489             self._tab.Activate()
    490             self._web_echo("Sanity_test")
    491             self._tab.WaitForDocumentReadyStateToBeComplete()
    492 
    493             # Video test must have the data from download test
    494             if ('video' in test_groups):
    495                 iv = test_groups.index('video')
    496                 if 'download' not in test_groups[:iv]:
    497                     msg = '"download" test must run before "video".'
    498                     raise error.TestError(msg)
    499 
    500             # Run all the test groups
    501             self._run_test_groups(test_groups)
    502 
    503         # Wrap up
    504         keyvals = self._plog.calc()
    505         keyvals.update(self._tmp_keyvals)
    506         keyvals.update(self._statomatic.publish())
    507 
    508         # check AC status is still the same as init
    509         self._power_status.refresh()
    510         on_ac = self._power_status.on_ac()
    511         if keyvals['b_on_ac'] != on_ac:
    512             raise error.TestError('on AC changed between start & stop of test')
    513 
    514         if not on_ac:
    515             whrs = self._power_status.battery[0].energy_full_design
    516             logging.info("energy_full_design = %0.3f Wh", whrs)
    517 
    518             # Calculate expected battery life time with ChromeVer power draw
    519             idle_name = 'ChromeVer_system_pwr'
    520             if idle_name in keyvals:
    521                 hours_life = whrs / keyvals[idle_name]
    522                 keyvals['hours_battery_ChromeVer'] = hours_life
    523 
    524             # Calculate a weighted power draw and battery life time. The weights
    525             # are intended to represent "typical" usage. Some video, some Flash
    526             # ... and most of the time idle. see,
    527             # http://www.chromium.org/chromium-os/testing/power-testing
    528             weights = {'vid400p_h264_system_pwr':0.1,
    529                        'BallsFlex_system_pwr':0.1,
    530                        'BallsDHTML_system_pwr':0.3,
    531                       }
    532             weights[idle_name] = 1 - sum(weights.values())
    533 
    534             if set(weights).issubset(set(keyvals)):
    535                 p = sum(w * keyvals[k] for (k, w) in weights.items())
    536                 keyvals['w_Weighted_system_pwr'] = p
    537                 keyvals['hours_battery_Weighted'] = whrs / p
    538 
    539         self.write_perf_keyval(keyvals)
    540         self._plog.save_results(self.resultsdir)
    541 
    542 
    543     def cleanup(self):
    544         # cleanup() is run by common_lib/test.py
    545         try:
    546             self._test_server.stop()
    547         except AttributeError:
    548             logging.debug('test_server could not be stopped in cleanup')
    549 
    550         if self._backlight:
    551             self._backlight.restore()
    552         if self._services:
    553             self._services.restore_services()
    554 
    555         super(power_Consumption, self).cleanup()
    556