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