1 # Copyright (c) 2013 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 """This is a client side WebGL aquarium test. 6 7 Description of some of the test result output: 8 - interframe time: The time elapsed between two frames. It is the elapsed 9 time between two consecutive calls to the render() function. 10 - render time: The time it takes in Javascript to construct a frame and 11 submit all the GL commands. It is the time it takes for a render() 12 function call to complete. 13 """ 14 15 import functools 16 import logging 17 import math 18 import os 19 import sampler 20 import system_sampler 21 import threading 22 import time 23 24 from autotest_lib.client.bin import fps_meter 25 from autotest_lib.client.bin import utils 26 from autotest_lib.client.common_lib import error 27 from autotest_lib.client.common_lib.cros import chrome 28 from autotest_lib.client.common_lib.cros import memory_eater 29 from autotest_lib.client.cros.graphics import graphics_utils 30 from autotest_lib.client.cros import service_stopper 31 from autotest_lib.client.cros.power import power_rapl, power_status, power_utils 32 33 # Minimum battery charge percentage to run the test 34 BATTERY_INITIAL_CHARGED_MIN = 10 35 36 # Measurement duration in seconds. 37 MEASUREMENT_DURATION = 30 38 39 POWER_DESCRIPTION = 'avg_energy_rate_1000_fishes' 40 41 # Time to exclude from calculation after playing a webgl demo [seconds]. 42 STABILIZATION_DURATION = 10 43 44 45 class graphics_WebGLAquarium(graphics_utils.GraphicsTest): 46 """WebGL aquarium graphics test.""" 47 version = 1 48 49 _backlight = None 50 _power_status = None 51 _service_stopper = None 52 _test_power = False 53 active_tab = None 54 flip_stats = {} 55 kernel_sampler = None 56 perf_keyval = {} 57 sampler_lock = None 58 test_duration_secs = 30 59 test_setting_num_fishes = 50 60 test_settings = { 61 50: ('setSetting2', 2), 62 1000: ('setSetting6', 6), 63 } 64 65 def setup(self): 66 """Testcase setup.""" 67 tarball_path = os.path.join(self.bindir, 68 'webgl_aquarium_static.tar.bz2') 69 utils.extract_tarball_to_dir(tarball_path, self.srcdir) 70 71 def initialize(self): 72 """Testcase initialization.""" 73 super(graphics_WebGLAquarium, self).initialize() 74 self.sampler_lock = threading.Lock() 75 # TODO: Create samplers for other platforms (e.g. x86). 76 if utils.get_board().lower() in ['daisy', 'daisy_spring']: 77 # Enable ExynosSampler on Exynos platforms. The sampler looks for 78 # exynos-drm page flip states: 'wait_kds', 'rendered', 'prepared', 79 # and 'flipped' in kernel debugfs. 80 81 # Sample 3-second durtaion for every 5 seconds. 82 self.kernel_sampler = sampler.ExynosSampler(period=5, duration=3) 83 self.kernel_sampler.sampler_callback = self.exynos_sampler_callback 84 self.kernel_sampler.output_flip_stats = ( 85 self.exynos_output_flip_stats) 86 87 def cleanup(self): 88 """Testcase cleanup.""" 89 if self._backlight: 90 self._backlight.restore() 91 if self._service_stopper: 92 self._service_stopper.restore_services() 93 super(graphics_WebGLAquarium, self).cleanup() 94 95 def setup_webpage(self, browser, test_url, num_fishes): 96 """Open fish tank in a new tab. 97 98 @param browser: The Browser object to run the test with. 99 @param test_url: The URL to the aquarium test site. 100 @param num_fishes: The number of fishes to run the test with. 101 """ 102 # Create tab and load page. Set the number of fishes when page is fully 103 # loaded. 104 tab = browser.tabs.New() 105 tab.Navigate(test_url) 106 tab.Activate() 107 self.active_tab = tab 108 tab.WaitForDocumentReadyStateToBeComplete() 109 110 # Set the number of fishes when document finishes loading. Also reset 111 # our own FPS counter and start recording FPS and rendering time. 112 utils.wait_for_value( 113 lambda: tab.EvaluateJavaScript( 114 'if (document.readyState === "complete") {' 115 ' setSetting(document.getElementById("%s"), %d);' 116 ' g_crosFpsCounter.reset();' 117 ' true;' 118 '} else {' 119 ' false;' 120 '}' % self.test_settings[num_fishes] 121 ), 122 expected_value=True, 123 timeout_sec=30) 124 125 return tab 126 127 def tear_down_webpage(self): 128 """Close the tab containing testing webpage.""" 129 # Do not close the tab when the sampler_callback is 130 # doing its work. 131 with self.sampler_lock: 132 self.active_tab.Close() 133 self.active_tab = None 134 135 def run_fish_test(self, browser, test_url, num_fishes, perf_log=True): 136 """Run the test with the given number of fishes. 137 138 @param browser: The Browser object to run the test with. 139 @param test_url: The URL to the aquarium test site. 140 @param num_fishes: The number of fishes to run the test with. 141 @param perf_log: Report perf data only if it's set to True. 142 """ 143 144 tab = self.setup_webpage(browser, test_url, num_fishes) 145 146 if self.kernel_sampler: 147 self.kernel_sampler.start_sampling_thread() 148 time.sleep(self.test_duration_secs) 149 if self.kernel_sampler: 150 self.kernel_sampler.stop_sampling_thread() 151 self.kernel_sampler.output_flip_stats('flip_stats_%d' % num_fishes) 152 self.flip_stats = {} 153 154 # Get average FPS and rendering time, then close the tab. 155 avg_fps = tab.EvaluateJavaScript('g_crosFpsCounter.getAvgFps();') 156 if math.isnan(float(avg_fps)): 157 raise error.TestFail('Failed: Could not get FPS count.') 158 159 avg_interframe_time = tab.EvaluateJavaScript( 160 'g_crosFpsCounter.getAvgInterFrameTime();') 161 avg_render_time = tab.EvaluateJavaScript( 162 'g_crosFpsCounter.getAvgRenderTime();') 163 std_interframe_time = tab.EvaluateJavaScript( 164 'g_crosFpsCounter.getStdInterFrameTime();') 165 std_render_time = tab.EvaluateJavaScript( 166 'g_crosFpsCounter.getStdRenderTime();') 167 self.perf_keyval['avg_fps_%04d_fishes' % num_fishes] = avg_fps 168 self.perf_keyval['avg_interframe_time_%04d_fishes' % num_fishes] = ( 169 avg_interframe_time) 170 self.perf_keyval['avg_render_time_%04d_fishes' % num_fishes] = ( 171 avg_render_time) 172 self.perf_keyval['std_interframe_time_%04d_fishes' % num_fishes] = ( 173 std_interframe_time) 174 self.perf_keyval['std_render_time_%04d_fishes' % num_fishes] = ( 175 std_render_time) 176 logging.info('%d fish(es): Average FPS = %f, ' 177 'average render time = %f', num_fishes, avg_fps, 178 avg_render_time) 179 180 if perf_log: 181 # Report frames per second to chromeperf/ dashboard. 182 self.output_perf_value( 183 description='avg_fps_%04d_fishes' % num_fishes, 184 value=avg_fps, 185 units='fps', 186 higher_is_better=True) 187 188 # Intel only: Record the power consumption for the next few seconds. 189 rapl_rate = power_rapl.get_rapl_measurement( 190 'rapl_%04d_fishes' % num_fishes) 191 # Remove entries that we don't care about. 192 rapl_rate = {key: rapl_rate[key] 193 for key in rapl_rate.keys() if key.endswith('pwr')} 194 # Report to chromeperf/ dashboard. 195 for key, values in rapl_rate.iteritems(): 196 self.output_perf_value( 197 description=key, 198 value=values, 199 units='W', 200 higher_is_better=False, 201 graph='rapl_power_consumption' 202 ) 203 204 def run_power_test(self, browser, test_url, ac_ok): 205 """Runs the webgl power consumption test and reports the perf results. 206 207 @param browser: The Browser object to run the test with. 208 @param test_url: The URL to the aquarium test site. 209 @param ac_ok: Boolean on whether its ok to have AC power supplied. 210 """ 211 212 self._backlight = power_utils.Backlight() 213 self._backlight.set_default() 214 215 self._service_stopper = service_stopper.ServiceStopper( 216 service_stopper.ServiceStopper.POWER_DRAW_SERVICES) 217 self._service_stopper.stop_services() 218 219 if not ac_ok: 220 self._power_status = power_status.get_status() 221 # Verify that we are running on battery and the battery is 222 # sufficiently charged. 223 self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN) 224 225 measurements = [ 226 power_status.SystemPower(self._power_status.battery_path) 227 ] 228 229 def get_power(): 230 power_logger = power_status.PowerLogger(measurements) 231 power_logger.start() 232 time.sleep(STABILIZATION_DURATION) 233 start_time = time.time() 234 time.sleep(MEASUREMENT_DURATION) 235 power_logger.checkpoint('result', start_time) 236 keyval = power_logger.calc() 237 logging.info('Power output %s', keyval) 238 return keyval['result_' + measurements[0].domain + '_pwr'] 239 240 self.run_fish_test(browser, test_url, 1000, perf_log=False) 241 if not ac_ok: 242 energy_rate = get_power() 243 # This is a power specific test so we are not capturing 244 # avg_fps and avg_render_time in this test. 245 self.perf_keyval[POWER_DESCRIPTION] = energy_rate 246 self.output_perf_value( 247 description=POWER_DESCRIPTION, 248 value=energy_rate, 249 units='W', 250 higher_is_better=False) 251 252 def exynos_sampler_callback(self, sampler_obj): 253 """Sampler callback function for ExynosSampler. 254 255 @param sampler_obj: The ExynosSampler object that invokes this callback 256 function. 257 """ 258 if sampler_obj.stopped: 259 return 260 261 with self.sampler_lock: 262 now = time.time() 263 results = {} 264 info_str = ['\nfb_id wait_kds flipped'] 265 for value in sampler_obj.frame_buffers.itervalues(): 266 results[value.fb] = {} 267 for state, stats in value.states.iteritems(): 268 results[value.fb][state] = (stats.avg, stats.stdev) 269 info_str.append('%s: %s %s' % (value.fb, 270 results[value.fb]['wait_kds'][0], 271 results[value.fb]['flipped'][0])) 272 results['avg_fps'] = self.active_tab.EvaluateJavaScript( 273 'g_crosFpsCounter.getAvgFps();') 274 results['avg_render_time'] = self.active_tab.EvaluateJavaScript( 275 'g_crosFpsCounter.getAvgRenderTime();') 276 self.active_tab.ExecuteJavaScript('g_crosFpsCounter.reset();') 277 info_str.append('avg_fps: %s, avg_render_time: %s' % 278 (results['avg_fps'], results['avg_render_time'])) 279 self.flip_stats[now] = results 280 logging.info('\n'.join(info_str)) 281 282 def exynos_output_flip_stats(self, file_name): 283 """Pageflip statistics output function for ExynosSampler. 284 285 @param file_name: The output file name. 286 """ 287 # output format: 288 # time fb_id avg_rendered avg_prepared avg_wait_kds avg_flipped 289 # std_rendered std_prepared std_wait_kds std_flipped 290 with open(file_name, 'w') as f: 291 for t in sorted(self.flip_stats.keys()): 292 if ('avg_fps' in self.flip_stats[t] and 293 'avg_render_time' in self.flip_stats[t]): 294 f.write('%s %s %s\n' % 295 (t, self.flip_stats[t]['avg_fps'], 296 self.flip_stats[t]['avg_render_time'])) 297 for fb, stats in self.flip_stats[t].iteritems(): 298 if not isinstance(fb, int): 299 continue 300 f.write('%s %s ' % (t, fb)) 301 f.write('%s %s %s %s ' % (stats['rendered'][0], 302 stats['prepared'][0], 303 stats['wait_kds'][0], 304 stats['flipped'][0])) 305 f.write('%s %s %s %s\n' % (stats['rendered'][1], 306 stats['prepared'][1], 307 stats['wait_kds'][1], 308 stats['flipped'][1])) 309 310 def write_samples(self, samples, filename): 311 """Writes all samples to result dir with the file name "samples'. 312 313 @param samples: A list of all collected samples. 314 @param filename: The file name to save under result directory. 315 """ 316 out_file = os.path.join(self.resultsdir, filename) 317 with open(out_file, 'w') as f: 318 for sample in samples: 319 print >> f, sample 320 321 def run_fish_test_with_memory_pressure( 322 self, browser, test_url, num_fishes, memory_pressure): 323 """Measure fps under memory pressure. 324 325 It measure FPS of WebGL aquarium while adding memory pressure. It runs 326 in 2 phases: 327 1. Allocate non-swappable memory until |memory_to_reserve_mb| is 328 remained. The memory is not accessed after allocated. 329 2. Run "active" memory consumer in the background. After allocated, 330 Its content is accessed sequentially by page and looped around 331 infinitely. 332 The second phase is opeared in two possible modes: 333 1. "single" mode, which means only one "active" memory consumer. After 334 running a single memory consumer with a given memory size, it waits 335 for a while to see if system can afford current memory pressure 336 (definition here is FPS > 5). If it does, kill current consumer and 337 launch another consumer with a larger memory size. The process keeps 338 going until system couldn't afford the load. 339 2. "multiple"mode. It simply launch memory consumers with a given size 340 one by one until system couldn't afford the load (e.g., FPS < 5). 341 In "single" mode, CPU load is lighter so we expect swap in/swap out 342 rate to be correlated to FPS better. In "multiple" mode, since there 343 are multiple busy loop processes, CPU pressure is another significant 344 cause of frame drop. Frame drop can happen easily due to busy CPU 345 instead of memory pressure. 346 347 @param browser: The Browser object to run the test with. 348 @param test_url: The URL to the aquarium test site. 349 @param num_fishes: The number of fishes to run the test with. 350 @param memory_pressure: Memory pressure parameters. 351 """ 352 consumer_mode = memory_pressure.get('consumer_mode', 'single') 353 memory_to_reserve_mb = memory_pressure.get('memory_to_reserve_mb', 500) 354 # Empirical number to quickly produce memory pressure. 355 if consumer_mode == 'single': 356 default_consumer_size_mb = memory_to_reserve_mb + 100 357 else: 358 default_consumer_size_mb = memory_to_reserve_mb / 2 359 consumer_size_mb = memory_pressure.get( 360 'consumer_size_mb', default_consumer_size_mb) 361 362 # Setup fish tank. 363 self.setup_webpage(browser, test_url, num_fishes) 364 365 # Drop all file caches. 366 utils.drop_caches() 367 368 def fps_near_zero(fps_sampler): 369 """Returns whether recent fps goes down to near 0. 370 371 @param fps_sampler: A system_sampler.Sampler object. 372 """ 373 last_fps = fps_sampler.get_last_avg_fps(6) 374 if last_fps: 375 logging.info('last fps %f', last_fps) 376 if last_fps <= 5: 377 return True 378 return False 379 380 max_allocated_mb = 0 381 # Consume free memory and release them by the end. 382 with memory_eater.consume_free_memory(memory_to_reserve_mb): 383 fps_sampler = system_sampler.SystemSampler( 384 memory_eater.MemoryEater.get_active_consumer_pids) 385 end_condition = functools.partial(fps_near_zero, fps_sampler) 386 with fps_meter.FPSMeter(fps_sampler.sample): 387 # Collects some samples before running memory pressure. 388 time.sleep(5) 389 try: 390 if consumer_mode == 'single': 391 # A single run couldn't generate samples representative 392 # enough. 393 # First runs squeeze more inactive anonymous memory into 394 # zram so in later runs we have a more stable memory 395 # stat. 396 max_allocated_mb = max( 397 memory_eater.run_single_memory_pressure( 398 consumer_size_mb, 100, end_condition, 10, 3, 399 900), 400 memory_eater.run_single_memory_pressure( 401 consumer_size_mb, 20, end_condition, 10, 3, 402 900), 403 memory_eater.run_single_memory_pressure( 404 consumer_size_mb, 10, end_condition, 10, 3, 405 900)) 406 elif consumer_mode == 'multiple': 407 max_allocated_mb = ( 408 memory_eater.run_multi_memory_pressure( 409 consumer_size_mb, end_condition, 10, 900)) 410 else: 411 raise error.TestFail( 412 'Failed: Unsupported consumer mode.') 413 except memory_eater.TimeoutException as e: 414 raise error.TestFail(e) 415 416 samples = fps_sampler.get_samples() 417 self.write_samples(samples, 'memory_pressure_fps_samples.txt') 418 419 self.perf_keyval['num_samples'] = len(samples) 420 self.perf_keyval['max_allocated_mb'] = max_allocated_mb 421 422 logging.info(self.perf_keyval) 423 424 self.output_perf_value( 425 description='max_allocated_mb_%d_fishes_reserved_%d_mb' % ( 426 num_fishes, memory_to_reserve_mb), 427 value=max_allocated_mb, 428 units='MB', 429 higher_is_better=True) 430 431 432 @graphics_utils.GraphicsTest.failure_report_decorator('graphics_WebGLAquarium') 433 def run_once(self, 434 test_duration_secs=30, 435 test_setting_num_fishes=(50, 1000), 436 power_test=False, 437 ac_ok=False, 438 memory_pressure=None): 439 """Find a browser with telemetry, and run the test. 440 441 @param test_duration_secs: The duration in seconds to run each scenario 442 for. 443 @param test_setting_num_fishes: A list of the numbers of fishes to 444 enable in the test. 445 @param power_test: Boolean on whether to run power_test 446 @param ac_ok: Boolean on whether its ok to have AC power supplied. 447 @param memory_pressure: A dictionay which specifies memory pressure 448 parameters: 449 'consumer_mode': 'single' or 'multiple' to have one or moultiple 450 concurrent memory consumers. 451 'consumer_size_mb': Amount of memory to allocate. In 'single' 452 mode, a single memory consumer would allocate memory by the 453 specific size. It then gradually allocates more memory until 454 FPS down to near 0. In 'multiple' mode, memory consumers of 455 this size would be spawn one by one until FPS down to near 0. 456 'memory_to_reserve_mb': Amount of memory to reserve before 457 running memory consumer. In practical we allocate mlocked 458 memory (i.e., not swappable) to consume free memory until this 459 amount of free memory remained. 460 """ 461 self.test_duration_secs = test_duration_secs 462 self.test_setting_num_fishes = test_setting_num_fishes 463 464 with chrome.Chrome(logged_in=False, init_network_controller=True) as cr: 465 cr.browser.platform.SetHTTPServerDirectories(self.srcdir) 466 test_url = cr.browser.platform.http_server.UrlOf( 467 os.path.join(self.srcdir, 'aquarium.html')) 468 469 if not utils.wait_for_idle_cpu(60.0, 0.1): 470 if not utils.wait_for_idle_cpu(20.0, 0.2): 471 raise error.TestFail('Failed: Could not get idle CPU.') 472 if not utils.wait_for_cool_machine(): 473 raise error.TestFail('Failed: Could not get cold machine.') 474 if memory_pressure: 475 self.run_fish_test_with_memory_pressure( 476 cr.browser, test_url, num_fishes=1000, 477 memory_pressure=memory_pressure) 478 self.tear_down_webpage() 479 elif power_test: 480 self._test_power = True 481 self.run_power_test(cr.browser, test_url, ac_ok) 482 self.tear_down_webpage() 483 else: 484 for n in self.test_setting_num_fishes: 485 self.run_fish_test(cr.browser, test_url, n) 486 self.tear_down_webpage() 487 self.write_perf_keyval(self.perf_keyval) 488