| # 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) |