# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """This is a client side WebGL aquarium test. Description of some of the test result output: - interframe time: The time elapsed between two frames. It is the elapsed time between two consecutive calls to the render() function. - render time: The time it takes in Javascript to construct a frame and submit all the GL commands. It is the time it takes for a render() function call to complete. """ import functools import logging import math import os import sampler import system_sampler import threading import time from autotest_lib.client.bin import fps_meter from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import chrome from autotest_lib.client.common_lib.cros import memory_eater from autotest_lib.client.cros.graphics import graphics_utils from autotest_lib.client.cros import perf from autotest_lib.client.cros import service_stopper from autotest_lib.client.cros.power import power_rapl, power_status, power_utils # Minimum battery charge percentage to run the test BATTERY_INITIAL_CHARGED_MIN = 10 # Measurement duration in seconds. MEASUREMENT_DURATION = 30 POWER_DESCRIPTION = 'avg_energy_rate_1000_fishes' # Time to exclude from calculation after playing a webgl demo [seconds]. STABILIZATION_DURATION = 10 class graphics_WebGLAquarium(graphics_utils.GraphicsTest): """WebGL aquarium graphics test.""" version = 1 _backlight = None _power_status = None _service_stopper = None _test_power = False active_tab = None flip_stats = {} kernel_sampler = None perf_keyval = {} sampler_lock = None test_duration_secs = 30 test_setting_num_fishes = 50 test_settings = { 50: ('setSetting2', 2), 1000: ('setSetting6', 6), } def setup(self): """Testcase setup.""" tarball_path = os.path.join(self.bindir, 'webgl_aquarium_static.tar.bz2') utils.extract_tarball_to_dir(tarball_path, self.srcdir) def initialize(self): """Testcase initialization.""" super(graphics_WebGLAquarium, self).initialize() self.sampler_lock = threading.Lock() # TODO: Create samplers for other platforms (e.g. x86). if utils.get_board().lower() in ['daisy', 'daisy_spring']: # Enable ExynosSampler on Exynos platforms. The sampler looks for # exynos-drm page flip states: 'wait_kds', 'rendered', 'prepared', # and 'flipped' in kernel debugfs. # Sample 3-second durtaion for every 5 seconds. self.kernel_sampler = sampler.ExynosSampler(period=5, duration=3) self.kernel_sampler.sampler_callback = self.exynos_sampler_callback self.kernel_sampler.output_flip_stats = ( self.exynos_output_flip_stats) def cleanup(self): """Testcase cleanup.""" if self._backlight: self._backlight.restore() if self._service_stopper: self._service_stopper.restore_services() super(graphics_WebGLAquarium, self).cleanup() def setup_webpage(self, browser, test_url, num_fishes): """Open fish tank in a new tab. @param browser: The Browser object to run the test with. @param test_url: The URL to the aquarium test site. @param num_fishes: The number of fishes to run the test with. """ # Create tab and load page. Set the number of fishes when page is fully # loaded. tab = browser.tabs.New() tab.Navigate(test_url) tab.Activate() self.active_tab = tab tab.WaitForDocumentReadyStateToBeComplete() # Set the number of fishes when document finishes loading. Also reset # our own FPS counter and start recording FPS and rendering time. utils.wait_for_value( lambda: tab.EvaluateJavaScript( 'if (document.readyState === "complete") {' ' setSetting(document.getElementById("%s"), %d);' ' g_crosFpsCounter.reset();' ' true;' '} else {' ' false;' '}' % self.test_settings[num_fishes] ), expected_value=True, timeout_sec=30) return tab def tear_down_webpage(self): """Close the tab containing testing webpage.""" # Do not close the tab when the sampler_callback is # doing its work. with self.sampler_lock: self.active_tab.Close() self.active_tab = None def run_fish_test(self, browser, test_url, num_fishes, perf_log=True): """Run the test with the given number of fishes. @param browser: The Browser object to run the test with. @param test_url: The URL to the aquarium test site. @param num_fishes: The number of fishes to run the test with. @param perf_log: Report perf data only if it's set to True. """ tab = self.setup_webpage(browser, test_url, num_fishes) if self.kernel_sampler: self.kernel_sampler.start_sampling_thread() time.sleep(self.test_duration_secs) if self.kernel_sampler: self.kernel_sampler.stop_sampling_thread() self.kernel_sampler.output_flip_stats('flip_stats_%d' % num_fishes) self.flip_stats = {} # Get average FPS and rendering time, then close the tab. avg_fps = tab.EvaluateJavaScript('g_crosFpsCounter.getAvgFps();') if math.isnan(float(avg_fps)): raise error.TestFail('Failed: Could not get FPS count.') avg_interframe_time = tab.EvaluateJavaScript( 'g_crosFpsCounter.getAvgInterFrameTime();') avg_render_time = tab.EvaluateJavaScript( 'g_crosFpsCounter.getAvgRenderTime();') std_interframe_time = tab.EvaluateJavaScript( 'g_crosFpsCounter.getStdInterFrameTime();') std_render_time = tab.EvaluateJavaScript( 'g_crosFpsCounter.getStdRenderTime();') self.perf_keyval['avg_fps_%04d_fishes' % num_fishes] = avg_fps self.perf_keyval['avg_interframe_time_%04d_fishes' % num_fishes] = ( avg_interframe_time) self.perf_keyval['avg_render_time_%04d_fishes' % num_fishes] = ( avg_render_time) self.perf_keyval['std_interframe_time_%04d_fishes' % num_fishes] = ( std_interframe_time) self.perf_keyval['std_render_time_%04d_fishes' % num_fishes] = ( std_render_time) logging.info('%d fish(es): Average FPS = %f, ' 'average render time = %f', num_fishes, avg_fps, avg_render_time) if perf_log: # Report frames per second to chromeperf/ dashboard. self.output_perf_value( description='avg_fps_%04d_fishes' % num_fishes, value=avg_fps, units='fps', higher_is_better=True) # Intel only: Record the power consumption for the next few seconds. rapl_rate = power_rapl.get_rapl_measurement( 'rapl_%04d_fishes' % num_fishes) # Remove entries that we don't care about. rapl_rate = {key: rapl_rate[key] for key in rapl_rate.keys() if key.endswith('pwr')} # Report to chromeperf/ dashboard. for key, values in rapl_rate.iteritems(): self.output_perf_value( description=key, value=values, units='W', higher_is_better=False, graph='rapl_power_consumption' ) def run_power_test(self, browser, test_url, ac_ok): """Runs the webgl power consumption test and reports the perf results. @param browser: The Browser object to run the test with. @param test_url: The URL to the aquarium test site. @param ac_ok: Boolean on whether its ok to have AC power supplied. """ self._backlight = power_utils.Backlight() self._backlight.set_default() self._service_stopper = service_stopper.ServiceStopper( service_stopper.ServiceStopper.POWER_DRAW_SERVICES) self._service_stopper.stop_services() if not ac_ok: self._power_status = power_status.get_status() # Verify that we are running on battery and the battery is # sufficiently charged. self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN) measurements = [ power_status.SystemPower(self._power_status.battery_path) ] def get_power(): power_logger = power_status.PowerLogger(measurements) power_logger.start() time.sleep(STABILIZATION_DURATION) start_time = time.time() time.sleep(MEASUREMENT_DURATION) power_logger.checkpoint('result', start_time) keyval = power_logger.calc() logging.info('Power output %s', keyval) return keyval['result_' + measurements[0].domain + '_pwr'] self.run_fish_test(browser, test_url, 1000, perf_log=False) if not ac_ok: energy_rate = get_power() # This is a power specific test so we are not capturing # avg_fps and avg_render_time in this test. self.perf_keyval[POWER_DESCRIPTION] = energy_rate self.output_perf_value( description=POWER_DESCRIPTION, value=energy_rate, units='W', higher_is_better=False) def exynos_sampler_callback(self, sampler_obj): """Sampler callback function for ExynosSampler. @param sampler_obj: The ExynosSampler object that invokes this callback function. """ if sampler_obj.stopped: return with self.sampler_lock: now = time.time() results = {} info_str = ['\nfb_id wait_kds flipped'] for value in sampler_obj.frame_buffers.itervalues(): results[value.fb] = {} for state, stats in value.states.iteritems(): results[value.fb][state] = (stats.avg, stats.stdev) info_str.append('%s: %s %s' % (value.fb, results[value.fb]['wait_kds'][0], results[value.fb]['flipped'][0])) results['avg_fps'] = self.active_tab.EvaluateJavaScript( 'g_crosFpsCounter.getAvgFps();') results['avg_render_time'] = self.active_tab.EvaluateJavaScript( 'g_crosFpsCounter.getAvgRenderTime();') self.active_tab.ExecuteJavaScript('g_crosFpsCounter.reset();') info_str.append('avg_fps: %s, avg_render_time: %s' % (results['avg_fps'], results['avg_render_time'])) self.flip_stats[now] = results logging.info('\n'.join(info_str)) def exynos_output_flip_stats(self, file_name): """Pageflip statistics output function for ExynosSampler. @param file_name: The output file name. """ # output format: # time fb_id avg_rendered avg_prepared avg_wait_kds avg_flipped # std_rendered std_prepared std_wait_kds std_flipped with open(file_name, 'w') as f: for t in sorted(self.flip_stats.keys()): if ('avg_fps' in self.flip_stats[t] and 'avg_render_time' in self.flip_stats[t]): f.write('%s %s %s\n' % (t, self.flip_stats[t]['avg_fps'], self.flip_stats[t]['avg_render_time'])) for fb, stats in self.flip_stats[t].iteritems(): if not isinstance(fb, int): continue f.write('%s %s ' % (t, fb)) f.write('%s %s %s %s ' % (stats['rendered'][0], stats['prepared'][0], stats['wait_kds'][0], stats['flipped'][0])) f.write('%s %s %s %s\n' % (stats['rendered'][1], stats['prepared'][1], stats['wait_kds'][1], stats['flipped'][1])) def write_samples(self, samples, filename): """Writes all samples to result dir with the file name "samples'. @param samples: A list of all collected samples. @param filename: The file name to save under result directory. """ out_file = os.path.join(self.resultsdir, filename) with open(out_file, 'w') as f: for sample in samples: print >> f, sample def run_fish_test_with_memory_pressure( self, browser, test_url, num_fishes, memory_pressure): """Measure fps under memory pressure. It measure FPS of WebGL aquarium while adding memory pressure. It runs in 2 phases: 1. Allocate non-swappable memory until |memory_to_reserve_mb| is remained. The memory is not accessed after allocated. 2. Run "active" memory consumer in the background. After allocated, Its content is accessed sequentially by page and looped around infinitely. The second phase is opeared in two possible modes: 1. "single" mode, which means only one "active" memory consumer. After running a single memory consumer with a given memory size, it waits for a while to see if system can afford current memory pressure (definition here is FPS > 5). If it does, kill current consumer and launch another consumer with a larger memory size. The process keeps going until system couldn't afford the load. 2. "multiple"mode. It simply launch memory consumers with a given size one by one until system couldn't afford the load (e.g., FPS < 5). In "single" mode, CPU load is lighter so we expect swap in/swap out rate to be correlated to FPS better. In "multiple" mode, since there are multiple busy loop processes, CPU pressure is another significant cause of frame drop. Frame drop can happen easily due to busy CPU instead of memory pressure. @param browser: The Browser object to run the test with. @param test_url: The URL to the aquarium test site. @param num_fishes: The number of fishes to run the test with. @param memory_pressure: Memory pressure parameters. """ consumer_mode = memory_pressure.get('consumer_mode', 'single') memory_to_reserve_mb = memory_pressure.get('memory_to_reserve_mb', 500) # Empirical number to quickly produce memory pressure. if consumer_mode == 'single': default_consumer_size_mb = memory_to_reserve_mb + 100 else: default_consumer_size_mb = memory_to_reserve_mb / 2 consumer_size_mb = memory_pressure.get( 'consumer_size_mb', default_consumer_size_mb) # Setup fish tank. self.setup_webpage(browser, test_url, num_fishes) # Drop all file caches. utils.drop_caches() def fps_near_zero(fps_sampler): """Returns whether recent fps goes down to near 0. @param fps_sampler: A system_sampler.Sampler object. """ last_fps = fps_sampler.get_last_avg_fps(6) if last_fps: logging.info('last fps %f', last_fps) if last_fps <= 5: return True return False max_allocated_mb = 0 # Consume free memory and release them by the end. with memory_eater.consume_free_memory(memory_to_reserve_mb): fps_sampler = system_sampler.SystemSampler( memory_eater.MemoryEater.get_active_consumer_pids) end_condition = functools.partial(fps_near_zero, fps_sampler) with fps_meter.FPSMeter(fps_sampler.sample): # Collects some samples before running memory pressure. time.sleep(5) try: if consumer_mode == 'single': # A single run couldn't generate samples representative # enough. # First runs squeeze more inactive anonymous memory into # zram so in later runs we have a more stable memory # stat. max_allocated_mb = max( memory_eater.run_single_memory_pressure( consumer_size_mb, 100, end_condition, 10, 3, 900), memory_eater.run_single_memory_pressure( consumer_size_mb, 20, end_condition, 10, 3, 900), memory_eater.run_single_memory_pressure( consumer_size_mb, 10, end_condition, 10, 3, 900)) elif consumer_mode == 'multiple': max_allocated_mb = ( memory_eater.run_multi_memory_pressure( consumer_size_mb, end_condition, 10, 900)) else: raise error.TestFail( 'Failed: Unsupported consumer mode.') except memory_eater.TimeoutException as e: raise error.TestFail(e) samples = fps_sampler.get_samples() self.write_samples(samples, 'memory_pressure_fps_samples.txt') self.perf_keyval['num_samples'] = len(samples) self.perf_keyval['max_allocated_mb'] = max_allocated_mb logging.info(self.perf_keyval) self.output_perf_value( description='max_allocated_mb_%d_fishes_reserved_%d_mb' % ( num_fishes, memory_to_reserve_mb), value=max_allocated_mb, units='MB', higher_is_better=True) @graphics_utils.GraphicsTest.failure_report_decorator('graphics_WebGLAquarium') def run_once(self, test_duration_secs=30, test_setting_num_fishes=(50, 1000), power_test=False, ac_ok=False, memory_pressure=None): """Find a browser with telemetry, and run the test. @param test_duration_secs: The duration in seconds to run each scenario for. @param test_setting_num_fishes: A list of the numbers of fishes to enable in the test. @param power_test: Boolean on whether to run power_test @param ac_ok: Boolean on whether its ok to have AC power supplied. @param memory_pressure: A dictionay which specifies memory pressure parameters: 'consumer_mode': 'single' or 'multiple' to have one or moultiple concurrent memory consumers. 'consumer_size_mb': Amount of memory to allocate. In 'single' mode, a single memory consumer would allocate memory by the specific size. It then gradually allocates more memory until FPS down to near 0. In 'multiple' mode, memory consumers of this size would be spawn one by one until FPS down to near 0. 'memory_to_reserve_mb': Amount of memory to reserve before running memory consumer. In practical we allocate mlocked memory (i.e., not swappable) to consume free memory until this amount of free memory remained. """ self.test_duration_secs = test_duration_secs self.test_setting_num_fishes = test_setting_num_fishes pc_error_reason = None with chrome.Chrome(logged_in=False, init_network_controller=True) as cr: cr.browser.platform.SetHTTPServerDirectories(self.srcdir) test_url = cr.browser.platform.http_server.UrlOf( os.path.join(self.srcdir, 'aquarium.html')) utils.report_temperature(self, 'temperature_1_start') # Wrap the test run inside of a PerfControl instance to make machine # behavior more consistent. with perf.PerfControl() as pc: if not pc.verify_is_valid(): raise error.TestFail('Failed: %s' % pc.get_error_reason()) utils.report_temperature(self, 'temperature_2_before_test') if memory_pressure: self.run_fish_test_with_memory_pressure( cr.browser, test_url, num_fishes=1000, memory_pressure=memory_pressure) self.tear_down_webpage() elif power_test: self._test_power = True self.run_power_test(cr.browser, test_url, ac_ok) self.tear_down_webpage() else: for n in self.test_setting_num_fishes: self.run_fish_test(cr.browser, test_url, n) self.tear_down_webpage() if not pc.verify_is_valid(): # Defer error handling until after perf report. pc_error_reason = pc.get_error_reason() utils.report_temperature(self, 'temperature_3_after_test') self.write_perf_keyval(self.perf_keyval) if pc_error_reason: raise error.TestWarn('Warning: %s' % pc_error_reason)