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