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 7Description 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 15import logging 16import math 17import os 18import sampler 19import threading 20import time 21 22from autotest_lib.client.bin import test, utils 23from autotest_lib.client.common_lib import error 24from autotest_lib.client.common_lib.cros import chrome 25from autotest_lib.client.cros.graphics import graphics_utils 26from autotest_lib.client.cros import power_status, power_utils 27from autotest_lib.client.cros import service_stopper 28 29# Minimum battery charge percentage to run the test 30BATTERY_INITIAL_CHARGED_MIN = 10 31 32# Measurement duration in seconds. 33MEASUREMENT_DURATION = 30 34 35POWER_DESCRIPTION = 'avg_energy_rate_1000_fishes' 36 37# Time to exclude from calculation after playing a webgl demo [seconds]. 38STABILIZATION_DURATION = 10 39 40 41class graphics_WebGLAquarium(test.test): 42 """WebGL aquarium graphics test.""" 43 version = 1 44 45 _backlight = None 46 _power_status = None 47 _service_stopper = None 48 _test_power = False 49 active_tab = None 50 flip_stats = {} 51 GSC = None 52 kernel_sampler = None 53 perf_keyval = {} 54 sampler_lock = None 55 test_duration_secs = 30 56 test_setting_num_fishes = 50 57 test_settings = { 58 50: ('setSetting2', 2), 59 1000: ('setSetting6', 6), 60 } 61 62 def setup(self): 63 tarball_path = os.path.join(self.bindir, 64 'webgl_aquarium_static.tar.bz2') 65 utils.extract_tarball_to_dir(tarball_path, self.srcdir) 66 67 def initialize(self): 68 self.GSC = graphics_utils.GraphicsStateChecker() 69 self.sampler_lock = threading.Lock() 70 # TODO: Create samplers for other platforms (e.g. x86). 71 if utils.get_board().lower() in ['daisy', 'daisy_spring']: 72 # Enable ExynosSampler on Exynos platforms. The sampler looks for 73 # exynos-drm page flip states: 'wait_kds', 'rendered', 'prepared', 74 # and 'flipped' in kernel debugfs. 75 76 # Sample 3-second durtaion for every 5 seconds. 77 self.kernel_sampler = sampler.ExynosSampler(period=5, duration=3) 78 self.kernel_sampler.sampler_callback = self.exynos_sampler_callback 79 self.kernel_sampler.output_flip_stats = ( 80 self.exynos_output_flip_stats) 81 82 def cleanup(self): 83 if self._backlight: 84 self._backlight.restore() 85 if self._service_stopper: 86 self._service_stopper.restore_services() 87 if self.GSC: 88 keyvals = self.GSC.get_memory_keyvals() 89 if not self._test_power: 90 for key, val in keyvals.iteritems(): 91 self.output_perf_value( 92 description=key, 93 value=val, 94 units='bytes', 95 higher_is_better=False) 96 self.GSC.finalize() 97 self.write_perf_keyval(keyvals) 98 99 def run_fish_test(self, browser, test_url, num_fishes, perf_log=True): 100 """Run the test with the given number of fishes. 101 102 @param browser: The Browser object to run the test with. 103 @param test_url: The URL to the aquarium test site. 104 @param num_fishes: The number of fishes to run the test with. 105 @param perf_log: Report perf data only if it's set to True. 106 """ 107 # Create tab and load page. Set the number of fishes when page is fully 108 # loaded. 109 tab = browser.tabs.New() 110 tab.Navigate(test_url) 111 tab.Activate() 112 self.active_tab = tab 113 tab.WaitForDocumentReadyStateToBeComplete() 114 115 # Set the number of fishes when document finishes loading. Also reset 116 # our own FPS counter and start recording FPS and rendering time. 117 utils.wait_for_value( 118 lambda: tab.EvaluateJavaScript('if (document.readyState === "complete") {' 119 ' setSetting(document.getElementById("%s"), %d);' 120 ' g_crosFpsCounter.reset();' 121 ' true;' 122 '} else {' 123 ' false;' 124 '}' % self.test_settings[num_fishes]), 125 expected_value=True, 126 timeout_sec=30) 127 128 if self.kernel_sampler: 129 self.kernel_sampler.start_sampling_thread() 130 time.sleep(self.test_duration_secs) 131 if self.kernel_sampler: 132 self.kernel_sampler.stop_sampling_thread() 133 self.kernel_sampler.output_flip_stats('flip_stats_%d' % num_fishes) 134 self.flip_stats = {} 135 136 # Get average FPS and rendering time, then close the tab. 137 avg_fps = tab.EvaluateJavaScript('g_crosFpsCounter.getAvgFps();') 138 if math.isnan(float(avg_fps)): 139 raise error.TestFail('Failed: Could not get FPS count.') 140 141 avg_interframe_time = tab.EvaluateJavaScript( 142 'g_crosFpsCounter.getAvgInterFrameTime();') 143 avg_render_time = tab.EvaluateJavaScript( 144 'g_crosFpsCounter.getAvgRenderTime();') 145 std_interframe_time = tab.EvaluateJavaScript( 146 'g_crosFpsCounter.getStdInterFrameTime();') 147 std_render_time = tab.EvaluateJavaScript( 148 'g_crosFpsCounter.getStdRenderTime();') 149 self.perf_keyval['avg_fps_%04d_fishes' % num_fishes] = avg_fps 150 self.perf_keyval['avg_interframe_time_%04d_fishes' % num_fishes] = ( 151 avg_interframe_time) 152 self.perf_keyval['avg_render_time_%04d_fishes' % num_fishes] = ( 153 avg_render_time) 154 self.perf_keyval['std_interframe_time_%04d_fishes' % num_fishes] = ( 155 std_interframe_time) 156 self.perf_keyval['std_render_time_%04d_fishes' % num_fishes] = ( 157 std_render_time) 158 logging.info('%d fish(es): Average FPS = %f, ' 159 'average render time = %f', num_fishes, avg_fps, 160 avg_render_time) 161 if perf_log: 162 self.output_perf_value( 163 description='avg_fps_%04d_fishes' % num_fishes, 164 value=avg_fps, 165 units='fps', 166 higher_is_better=True) 167 168 def run_power_test(self, browser, test_url, ac_ok): 169 """Runs the webgl power consumption test and reports the perf results. 170 171 @param browser: The Browser object to run the test with. 172 @param test_url: The URL to the aquarium test site. 173 @param ac_ok: Boolean on whether its ok to have AC power supplied. 174 """ 175 176 self._backlight = power_utils.Backlight() 177 self._backlight.set_default() 178 179 self._service_stopper = service_stopper.ServiceStopper( 180 service_stopper.ServiceStopper.POWER_DRAW_SERVICES) 181 self._service_stopper.stop_services() 182 183 if not ac_ok: 184 self._power_status = power_status.get_status() 185 # Verify that we are running on battery and the battery is 186 # sufficiently charged. 187 self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN) 188 189 measurements = [ 190 power_status.SystemPower(self._power_status.battery_path) 191 ] 192 193 def get_power(): 194 power_logger = power_status.PowerLogger(measurements) 195 power_logger.start() 196 time.sleep(STABILIZATION_DURATION) 197 start_time = time.time() 198 time.sleep(MEASUREMENT_DURATION) 199 power_logger.checkpoint('result', start_time) 200 keyval = power_logger.calc() 201 logging.info('Power output %s', keyval) 202 return keyval['result_' + measurements[0].domain + '_pwr'] 203 204 self.run_fish_test(browser, test_url, 1000, perf_log=False) 205 if not ac_ok: 206 energy_rate = get_power() 207 # This is a power specific test so we are not capturing 208 # avg_fps and avg_render_time in this test. 209 self.perf_keyval[POWER_DESCRIPTION] = energy_rate 210 self.output_perf_value( 211 description=POWER_DESCRIPTION, 212 value=energy_rate, 213 units='W', 214 higher_is_better=False) 215 216 def exynos_sampler_callback(self, sampler_obj): 217 """Sampler callback function for ExynosSampler. 218 219 @param sampler_obj: The ExynosSampler object that invokes this callback 220 function. 221 """ 222 if sampler_obj.stopped: 223 return 224 225 with self.sampler_lock: 226 now = time.time() 227 results = {} 228 info_str = ['\nfb_id wait_kds flipped'] 229 for value in sampler_obj.frame_buffers.itervalues(): 230 results[value.fb] = {} 231 for state, stats in value.states.iteritems(): 232 results[value.fb][state] = (stats.avg, stats.stdev) 233 info_str.append('%s: %s %s' % (value.fb, 234 results[value.fb]['wait_kds'][0], 235 results[value.fb]['flipped'][0])) 236 results['avg_fps'] = self.active_tab.EvaluateJavaScript( 237 'g_crosFpsCounter.getAvgFps();') 238 results['avg_render_time'] = self.active_tab.EvaluateJavaScript( 239 'g_crosFpsCounter.getAvgRenderTime();') 240 self.active_tab.ExecuteJavaScript('g_crosFpsCounter.reset();') 241 info_str.append('avg_fps: %s, avg_render_time: %s' % 242 (results['avg_fps'], results['avg_render_time'])) 243 self.flip_stats[now] = results 244 logging.info('\n'.join(info_str)) 245 246 def exynos_output_flip_stats(self, file_name): 247 """Pageflip statistics output function for ExynosSampler. 248 249 @param file_name: The output file name. 250 """ 251 # output format: 252 # time fb_id avg_rendered avg_prepared avg_wait_kds avg_flipped 253 # std_rendered std_prepared std_wait_kds std_flipped 254 with open(file_name, 'w') as f: 255 for t in sorted(self.flip_stats.keys()): 256 if ('avg_fps' in self.flip_stats[t] and 257 'avg_render_time' in self.flip_stats[t]): 258 f.write('%s %s %s\n' % 259 (t, self.flip_stats[t]['avg_fps'], 260 self.flip_stats[t]['avg_render_time'])) 261 for fb, stats in self.flip_stats[t].iteritems(): 262 if not isinstance(fb, int): 263 continue 264 f.write('%s %s ' % (t, fb)) 265 f.write('%s %s %s %s ' % (stats['rendered'][0], 266 stats['prepared'][0], 267 stats['wait_kds'][0], 268 stats['flipped'][0])) 269 f.write('%s %s %s %s\n' % (stats['rendered'][1], 270 stats['prepared'][1], 271 stats['wait_kds'][1], 272 stats['flipped'][1])) 273 274 def run_once(self, 275 test_duration_secs=30, 276 test_setting_num_fishes=(50, 1000), 277 power_test=False, 278 ac_ok=False): 279 """Find a brower with telemetry, and run the test. 280 281 @param test_duration_secs: The duration in seconds to run each scenario 282 for. 283 @param test_setting_num_fishes: A list of the numbers of fishes to 284 enable in the test. 285 @param power_test: Boolean on whether to run power_test 286 @param ac_ok: Boolean on whether its ok to have AC power supplied. 287 """ 288 self.test_duration_secs = test_duration_secs 289 self.test_setting_num_fishes = test_setting_num_fishes 290 291 with chrome.Chrome(logged_in=False, init_network_controller=True) as cr: 292 cr.browser.platform.SetHTTPServerDirectories(self.srcdir) 293 test_url = cr.browser.platform.http_server.UrlOf( 294 os.path.join(self.srcdir, 'aquarium.html')) 295 296 if not utils.wait_for_idle_cpu(60.0, 0.1): 297 if not utils.wait_for_idle_cpu(20.0, 0.2): 298 raise error.TestFail('Failed: Could not get idle CPU.') 299 if not utils.wait_for_cool_machine(): 300 raise error.TestFail('Failed: Could not get cold machine.') 301 if power_test: 302 self._test_power = True 303 self.run_power_test(cr.browser, test_url, ac_ok) 304 with self.sampler_lock: 305 self.active_tab.Close() 306 self.active_tab = None 307 else: 308 for n in self.test_setting_num_fishes: 309 self.run_fish_test(cr.browser, test_url, n) 310 # Do not close the tab when the sampler_callback is 311 # doing his work. 312 with self.sampler_lock: 313 self.active_tab.Close() 314 self.active_tab = None 315 self.write_perf_keyval(self.perf_keyval) 316