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