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