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 site_utils, 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 # 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 self._test_server.run() 81 82 logging.info('initialize() finished') 83 84 85 def _download_test_data(self): 86 """Download audio and video files. 87 88 This is also used as payload for download test. 89 90 Note, can reach payload via browser at 91 https://console.developers.google.com/storage/chromeos-test-public/big_buck_bunny 92 Start with README 93 """ 94 95 repo = 'http://commondatastorage.googleapis.com/chromeos-test-public/' 96 file_list = [repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.mp4', ] 97 if not self.short: 98 file_list += [ 99 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.ogg', 100 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp8.webm', 101 repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp9.webm', 102 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.mp4', 103 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.ogg', 104 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp8.webm', 105 repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp9.webm', 106 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.mp4', 107 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.ogg', 108 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp8.webm', 109 repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp9.webm', 110 repo + 'wikimedia/Greensleeves.ogg', 111 ] 112 113 for url in file_list: 114 logging.info('Downloading %s', url) 115 utils.unmap_url('', url, self._media_dir) 116 117 118 def _toggle_fullscreen(self): 119 """Toggle full screen mode.""" 120 # Note: full screen mode toggled with F11 is different from clicking the 121 # full screen icon on video player controls. This needs improvement. 122 # Bug: http://crbug.com/248939 123 graphics_utils.screen_toggle_fullscreen() 124 125 126 # Below are a series of generic sub-test runners. They run a given task 127 # and record the task name and start-end timestamps for future computation 128 # of power consumption during the task. 129 def _run_func(self, name, func, repeat=1, save_checkpoint=True): 130 """Run a given python function as a sub-test.""" 131 start_time = time.time() + self._stabilization_seconds 132 for _ in xrange(repeat): 133 ret = func() 134 if save_checkpoint: 135 self._plog.checkpoint(name, start_time) 136 return ret 137 138 139 def _run_sleep(self, name, seconds=60): 140 """Just sleep and record it as a named sub-test""" 141 start_time = time.time() + self._stabilization_seconds 142 time.sleep(seconds) 143 self._plog.checkpoint(name, start_time) 144 145 146 def _run_cmd(self, name, cmd, repeat=1): 147 """Run command in a shell as a sub-test""" 148 start_time = time.time() + self._stabilization_seconds 149 for _ in xrange(repeat): 150 logging.info('Executing command: %s', cmd) 151 exit_status = utils.system(cmd, ignore_status=True) 152 if exit_status != 0: 153 logging.error('run_cmd: the following command terminated with' 154 'a non zero exit status: %s', cmd) 155 self._plog.checkpoint(name, start_time) 156 return exit_status 157 158 159 def _run_until(self, name, predicate, timeout=60): 160 """Probe the |predicate| function and wait until it returns true. 161 Record the waiting time as a sub-test 162 """ 163 start_time = time.time() + self._stabilization_seconds 164 utils.poll_for_condition(predicate, timeout=timeout) 165 self._plog.checkpoint(name, start_time) 166 167 168 def _run_url(self, name, url, duration): 169 """Navigate to URL, sleep for some time and record it as a sub-test.""" 170 logging.info('Navigating to %s', url) 171 self._tab.Activate() 172 self._tab.Navigate(url) 173 self._run_sleep(name, duration) 174 tab_title = self._tab.EvaluateJavaScript('document.title') 175 logging.info('Sub-test name: %s Tab title: %s.', name, tab_title) 176 177 178 def _run_url_bg(self, name, url, duration): 179 """Run a web site in background tab. 180 181 Navigate to the given URL, open an empty tab to put the one with the 182 URL in background, then sleep and record it as a sub-test. 183 184 Args: 185 name: sub-test name. 186 url: url to open in background tab. 187 duration: number of seconds to sleep while taking measurements. 188 """ 189 bg_tab = self._tab 190 bg_tab.Navigate(url) 191 # Let it load and settle 192 time.sleep(self._stabilization_seconds / 2.) 193 tab_title = bg_tab.EvaluateJavaScript('document.title') 194 logging.info('App name: %s Tab title: %s.', name, tab_title) 195 # Open a new empty tab to cover the one with test payload. 196 fg_tab = self._browser.tabs.New() 197 fg_tab.Activate() 198 self._run_sleep(name, duration) 199 fg_tab.Close() 200 bg_tab.Activate() 201 202 203 def _run_group_download(self): 204 """Download over ethernet. Using video test data as payload.""" 205 206 # For short run, the payload is too small to take measurement 207 self._run_func('download_eth', 208 self._download_test_data , 209 repeat=self._repeats, 210 save_checkpoint=not(self.short)) 211 212 213 def _run_group_webpages(self): 214 """Runs a series of web pages as sub-tests.""" 215 data_url = self._url_base + self._static_sub_dir + '/' 216 217 # URLs to be only tested in foreground tab. 218 # Can't use about:blank here - crbug.com/248945 219 # but chrome://version is just as good for our needs. 220 urls = [('ChromeVer', 'chrome://version/')] 221 # URLs to be tested in both, background and foreground modes. 222 bg_urls = [] 223 224 more_urls = [('BallsDHTML', 225 data_url + 'balls/DHTMLBalls/dhtml.htm'), 226 # Disabling FlexBalls as experiment http://crbug.com/309403 227 # ('BallsFlex', 228 # data_url + 'balls/FlexBalls/flexballs.html'), 229 ] 230 231 if self.short: 232 urls += more_urls 233 else: 234 bg_urls += more_urls 235 bg_urls += [('Parapluesch', 236 'http://www.parapluesch.de/whiskystore/test.htm'), 237 ('PosterCircle', 238 'http://www.webkit.org' 239 '/blog-files/3d-transforms/poster-circle.html'), ] 240 241 for name, url in urls + bg_urls: 242 self._run_url(name, url, duration=self._duration_secs) 243 244 for name, url in bg_urls: 245 self._run_url_bg('bg_' + name, url, duration=self._duration_secs) 246 247 248 def _run_group_v8(self): 249 """Run the V8 benchmark suite as a sub-test. 250 251 Fire it up and wait until it displays "Score". 252 """ 253 254 url = 'http://v8.googlecode.com/svn/data/benchmarks/v7/run.html' 255 js = "document.getElementById('status').textContent" 256 tab = self._tab 257 258 def v8_func(): 259 """To be passed as the callable to self._run_func()""" 260 tab.Navigate(url) 261 # V8 test will usually take 17-25 seconds. Need some sleep here 262 # to let the V8 page load and create the 'status' div. 263 is_done = lambda: tab.EvaluateJavaScript(js).startswith('Score') 264 time.sleep(self._stabilization_seconds) 265 utils.poll_for_condition(is_done, timeout=60, desc='V8 score found') 266 267 self._run_func('V8', v8_func, repeat=self._repeats) 268 269 # Write v8 score from the last run to log 270 score = tab.EvaluateJavaScript(js) 271 score = score.strip().split()[1] 272 logging.info('V8 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 = site_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 wifi_ap = 'GoogleGuest' 377 wifi_sec = 'none' 378 wifi_pw = '' 379 380 flim = flimflam.FlimFlam() 381 conn = flim.ConnectService(retries=3, 382 retry=True, 383 service_type='wifi', 384 ssid=wifi_ap, 385 security=wifi_sec, 386 passphrase=wifi_pw, 387 mode='managed') 388 if not conn[0]: 389 logging.error("Could not connect to WiFi") 390 return 391 392 logging.info('Starting Backchannel') 393 with backchannel.Backchannel(): 394 # Wifi needs some time to recover after backchanel is activated 395 # TODO (kamrik) remove this sleep, once backchannel handles this 396 time.sleep(15) 397 398 cmd = 'ping -c %s www.google.com' % (self._duration_secs) 399 self._run_cmd('ping_wifi', cmd) 400 401 # This URL must be visible from WiFi network used for test 402 big_file_url = ('http://googleappengine.googlecode.com' 403 '/files/GoogleAppEngine-1.6.2.msi') 404 cmd = 'curl %s > /dev/null' % big_file_url 405 self._run_cmd('download_wifi', cmd, repeat=self._repeats) 406 407 408 def _run_group_backlight(self): 409 """Vary backlight brightness and record power at each setting.""" 410 for i in [100, 50, 0]: 411 self._backlight.set_percent(i) 412 start_time = time.time() + self._stabilization_seconds 413 time.sleep(30 * self._repeats) 414 self._plog.checkpoint('backlight_%03d' % i, start_time) 415 self._backlight.set_default() 416 417 418 def _web_echo(self, msg): 419 """ Displays a message in the browser.""" 420 url = self._url_base + 'echo.html?' 421 url += urllib.quote(msg) 422 self._tab.Navigate(url) 423 424 425 def _run_test_groups(self, groups): 426 """ Run all the test groups. 427 428 Args: 429 groups: list of sub-test groups to run. Each sub-test group refers 430 to a _run_group_...() function. 431 """ 432 433 for group in groups: 434 logging.info('Running group %s', group) 435 # The _web_echo here is important for some tests (esp. non UI) 436 # it gets the previous web page replaced with an almost empty one. 437 self._tab.Activate() 438 self._web_echo('Running test %s' % group) 439 test_func = getattr(self, '_run_group_%s' % group) 440 test_func() 441 442 443 def run_once(self, short=False, test_groups=None, reps=1): 444 # Some sub-tests have duration specified directly, _base_secs * reps 445 # is used in this case. Others complete whenever the underlying task 446 # completes, those are manually tuned to be roughly around 447 # reps * 30 seconds. Don't change _base_secs unless you also 448 # change the manual tuning in sub-tests 449 self._base_secs = 30 450 self._repeats = reps 451 self._duration_secs = self._base_secs * reps 452 453 # Lists of default tests to run 454 UI_TESTS = ['backlight', 'download', 'webpages', 'video', 'v8'] 455 NONUI_TESTS = ['backchannel', 'sound', 'lowlevel'] 456 DEFAULT_TESTS = UI_TESTS + NONUI_TESTS 457 DEFAULT_SHORT_TESTS = ['download', 'webpages', 'video'] 458 459 self.short = short 460 if test_groups is None: 461 if self.short: 462 test_groups = DEFAULT_SHORT_TESTS 463 else: 464 test_groups = DEFAULT_TESTS 465 logging.info('Test groups to run: %s', ', '.join(test_groups)) 466 467 self._backlight = power_utils.Backlight() 468 self._backlight.set_default() 469 470 measurements = \ 471 [power_status.SystemPower(self._power_status.battery_path)] 472 if power_utils.has_rapl_support(): 473 measurements += power_rapl.create_rapl() 474 self._plog = power_status.PowerLogger(measurements) 475 self._plog.start() 476 477 # Log in. 478 with chrome.Chrome() as cr: 479 self._browser = cr.browser 480 graphics_utils.screen_disable_energy_saving() 481 # Most of the tests will be running in this tab. 482 self._tab = cr.browser.tabs[0] 483 484 # Verify that we have a functioning browser and local web server. 485 self._tab.Activate() 486 self._web_echo("Sanity_test") 487 self._tab.WaitForDocumentReadyStateToBeComplete() 488 489 # Video test must have the data from download test 490 if ('video' in test_groups): 491 iv = test_groups.index('video') 492 if 'download' not in test_groups[:iv]: 493 msg = '"download" test must run before "video".' 494 raise error.TestError(msg) 495 496 # Run all the test groups 497 self._run_test_groups(test_groups) 498 499 # Wrap up 500 keyvals = self._plog.calc() 501 keyvals.update(self._tmp_keyvals) 502 503 # Calculate expected battery life time with ChromeVer power draw 504 idle_name = 'ChromeVer_system_pwr' 505 if idle_name in keyvals: 506 hours_life = self.energy_full_design / keyvals[idle_name] 507 keyvals['hours_battery_ChromeVer'] = hours_life 508 509 # Calculate a weighted power draw and battery life time. The weights 510 # are intended to represent "typical" usage. Some video, some Flash ... 511 # and most of the time idle. 512 # see http://www.chromium.org/chromium-os/testing/power-testing 513 weights = {'vid400p_h264_system_pwr':0.1, 514 # TODO(chromium:309403) re-enable BallsFlex once Flash in 515 # test-lab understood and re-distribute back to 60/20/10/10. 516 # 'BallsFlex_system_pwr':0.1, 517 'BallsDHTML_system_pwr':0.3, 518 } 519 weights[idle_name] = 1 - sum(weights.values()) 520 521 if set(weights).issubset(set(keyvals)): 522 p = sum(w * keyvals[k] for (k, w) in weights.items()) 523 keyvals['w_Weighted_system_pwr'] = p 524 keyvals['hours_battery_Weighted'] = self.energy_full_design / p 525 526 self.write_perf_keyval(keyvals) 527 self._plog.save_results(self.resultsdir) 528 529 530 def cleanup(self): 531 # cleanup() is run by common_lib/test.py 532 try: 533 self._test_server.stop() 534 except AttributeError: 535 logging.debug('test_server could not be stopped in cleanup') 536 537 if self._backlight: 538 self._backlight.restore() 539 if self._services: 540 self._services.restore_services() 541 542 super(power_Consumption, self).cleanup() 543