1 # Copyright 2018 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 """Test multiple WebGL windows spread across internal and external displays.""" 6 7 import collections 8 import logging 9 import os 10 import tarfile 11 import time 12 13 from autotest_lib.client.common_lib import error 14 from autotest_lib.client.cros import constants 15 from autotest_lib.client.cros.chameleon import chameleon_port_finder 16 from autotest_lib.client.cros.chameleon import chameleon_screen_test 17 from autotest_lib.server import test 18 from autotest_lib.server import utils 19 from autotest_lib.server.cros.multimedia import remote_facade_factory 20 21 22 class graphics_MultipleDisplays(test.test): 23 """Loads multiple WebGL windows on internal and external displays. 24 25 This test first initializes the extended Chameleon display. It then 26 launches four WebGL windows, two on each display. 27 """ 28 version = 1 29 WAIT_AFTER_SWITCH = 5 30 FPS_MEASUREMENT_DURATION = 15 31 STUCK_FPS_THRESHOLD = 2 32 MAXIMUM_STUCK_MEASUREMENTS = 5 33 34 # Running the HTTP server requires starting Chrome with 35 # init_network_controller set to True. 36 CHROME_KWARGS = {'extension_paths': [constants.AUDIO_TEST_EXTENSION, 37 constants.DISPLAY_TEST_EXTENSION], 38 'autotest_ext': True, 39 'init_network_controller': True} 40 41 # Local WebGL tarballs to populate the webroot. 42 STATIC_CONTENT = ['webgl_aquarium_static.tar.bz2', 43 'webgl_blob_static.tar.bz2'] 44 # Client directory for the root of the HTTP server 45 CLIENT_TEST_ROOT = \ 46 '/usr/local/autotest/tests/graphics_MultipleDisplays/webroot' 47 # Paths to later convert to URLs 48 WEBGL_AQUARIUM_PATH = \ 49 CLIENT_TEST_ROOT + '/webgl_aquarium_static/aquarium.html' 50 WEBGL_BLOB_PATH = CLIENT_TEST_ROOT + '/webgl_blob_static/blob.html' 51 52 MEDIA_CONTENT_BASE = ('https://commondatastorage.googleapis.com' 53 '/chromiumos-test-assets-public') 54 H264_URL = MEDIA_CONTENT_BASE + '/Shaka-Dash/1080_60.mp4' 55 VP9_URL = MEDIA_CONTENT_BASE + '/Shaka-Dash/1080_60.webm' 56 57 # Simple configuration to capture window position, content URL, or local 58 # path. Positioning is either internal or external and left or right half 59 # of the display. As an example, to open the newtab page on the left 60 # half: WindowConfig(True, True, 'chrome://newtab', None). 61 WindowConfig = collections.namedtuple( 62 'WindowConfig', 'internal_display, snap_left, url, path') 63 64 WINDOW_CONFIGS = \ 65 {'aquarium+blob': [WindowConfig(True, True, None, WEBGL_AQUARIUM_PATH), 66 WindowConfig(True, False, None, WEBGL_BLOB_PATH), 67 WindowConfig(False, True, None, WEBGL_AQUARIUM_PATH), 68 WindowConfig(False, False, None, WEBGL_BLOB_PATH)], 69 'aquarium+vp9+blob+h264': 70 [WindowConfig(True, True, None, WEBGL_AQUARIUM_PATH), 71 WindowConfig(True, False, VP9_URL, None), 72 WindowConfig(False, True, None, WEBGL_BLOB_PATH), 73 WindowConfig(False, False, H264_URL, None)]} 74 75 76 def _prepare_test_assets(self): 77 """Create a local test bundle and send it to the client. 78 79 @raise ValueError if the HTTP server does not start. 80 """ 81 # Create a directory to unpack archives. 82 temp_bundle_dir = utils.get_tmp_dir() 83 84 for static_content in self.STATIC_CONTENT: 85 archive_path = os.path.join(self.bindir, 'files', static_content) 86 87 with tarfile.open(archive_path, 'r') as tar: 88 tar.extractall(temp_bundle_dir) 89 90 # Send bundle to client. The extra slash is to send directory contents. 91 self._host.run('mkdir -p {}'.format(self.CLIENT_TEST_ROOT)) 92 self._host.send_file(temp_bundle_dir + '/', self.CLIENT_TEST_ROOT, 93 delete_dest=True) 94 95 # Start the HTTP server 96 res = self._browser_facade.set_http_server_directories( 97 self.CLIENT_TEST_ROOT) 98 if not res: 99 raise ValueError('HTTP server failed to start.') 100 101 def _calculate_new_bounds(self, config): 102 """Calculates bounds for 'snapping' to the left or right of a display. 103 104 @param config: WindowConfig specifying which display and side. 105 106 @return Dictionary with keys top, left, width, and height for the new 107 window boundaries. 108 """ 109 new_bounds = {'top': 0, 'left': 0, 'width': 0, 'height': 0} 110 display_info = filter( 111 lambda d: d.is_internal == config.internal_display, 112 self._display_facade.get_display_info()) 113 display_info = display_info[0] 114 115 # Since we are "snapping" windows left and right, set the width to half 116 # and set the height to the full working area. 117 new_bounds['width'] = int(display_info.work_area.width / 2) 118 new_bounds['height'] = display_info.work_area.height 119 120 # To specify the left or right "snap", first set the left edge to the 121 # display boundary. Note that for the internal display this will be 0. 122 # For the external display it will already include the offset from the 123 # internal display. Finally, if we are positioning to the right half 124 # of the display also add in the width. 125 new_bounds['left'] = display_info.bounds.left 126 if not config.snap_left: 127 new_bounds['left'] = new_bounds['left'] + new_bounds['width'] 128 129 return new_bounds 130 131 def _measure_external_display_fps(self, chameleon_port): 132 """Measure the update rate of the external display. 133 134 @param chameleon_port: ChameleonPort object for recording. 135 136 @raise ValueError if Chameleon FPS measurements indicate the external 137 display was not changing. 138 """ 139 chameleon_port.start_capturing_video() 140 time.sleep(self.FPS_MEASUREMENT_DURATION) 141 chameleon_port.stop_capturing_video() 142 143 # FPS information for saving later 144 self._fps_list = chameleon_port.get_captured_fps_list() 145 146 stuck_fps_list = filter(lambda fps: fps < self.STUCK_FPS_THRESHOLD, 147 self._fps_list) 148 if len(stuck_fps_list) > self.MAXIMUM_STUCK_MEASUREMENTS: 149 msg = 'Too many measurements {} are < {} FPS. GPU hang?'.format( 150 self._fps_list, self.STUCK_FPS_THRESHOLD) 151 raise ValueError(msg) 152 153 def _setup_windows(self): 154 """Create windows and update their positions. 155 156 @raise ValueError if the selected subtest is not a valid configuration. 157 @raise ValueError if a window configurations is invalid. 158 """ 159 160 if self._subtest not in self.WINDOW_CONFIGS: 161 msg = '{} is not a valid subtest. Choices are {}.'.format( 162 self._subtest, self.WINDOW_CONFIGS.keys()) 163 raise ValueError(msg) 164 165 for window_config in self.WINDOW_CONFIGS[self._subtest]: 166 url = window_config.url 167 if not url: 168 if not window_config.path: 169 msg = 'Path & URL not configured. {}'.format(window_config) 170 raise ValueError(msg) 171 172 # Convert the locally served content path to a URL. 173 url = self._browser_facade.http_server_url_of( 174 window_config.path) 175 176 new_bounds = self._calculate_new_bounds(window_config) 177 new_id = self._display_facade.create_window(url) 178 self._display_facade.update_window(new_id, 'normal', new_bounds) 179 time.sleep(self.WAIT_AFTER_SWITCH) 180 181 def run_once(self, host, subtest, test_duration=60): 182 self._host = host 183 self._subtest = subtest 184 185 factory = remote_facade_factory.RemoteFacadeFactory(host) 186 self._browser_facade = factory.create_browser_facade() 187 self._browser_facade.start_custom_chrome(self.CHROME_KWARGS) 188 self._display_facade = factory.create_display_facade() 189 self._graphics_facade = factory.create_graphics_facade() 190 191 logging.info('Preparing local WebGL test assets.') 192 self._prepare_test_assets() 193 194 chameleon_board = host.chameleon 195 chameleon_board.setup_and_reset(self.outputdir) 196 finder = chameleon_port_finder.ChameleonVideoInputFinder( 197 chameleon_board, self._display_facade) 198 199 # Snapshot the DUT system logs for any prior GPU hangs 200 self._graphics_facade.graphics_state_checker_initialize() 201 202 for chameleon_port in finder.iterate_all_ports(): 203 logging.info('Setting Chameleon screen to extended mode.') 204 self._display_facade.set_mirrored(False) 205 time.sleep(self.WAIT_AFTER_SWITCH) 206 207 logging.info('Launching WebGL windows.') 208 self._setup_windows() 209 210 logging.info('Measuring the external display update rate.') 211 self._measure_external_display_fps(chameleon_port) 212 213 logging.info('Running test for {}s.'.format(test_duration)) 214 time.sleep(test_duration) 215 216 # Raise an error on new GPU hangs 217 self._graphics_facade.graphics_state_checker_finalize() 218 219 def postprocess_iteration(self): 220 desc = 'Display update rate {}'.format(self._subtest) 221 self.output_perf_value(description=desc, value=self._fps_list, 222 units='FPS', higher_is_better=True, graph=None) 223