# Copyright 2018 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. """Test multiple WebGL windows spread across internal and external displays.""" import collections import logging import os import tarfile import time from autotest_lib.client.common_lib import error from autotest_lib.client.cros import constants from autotest_lib.client.cros.chameleon import chameleon_port_finder from autotest_lib.client.cros.chameleon import chameleon_screen_test from autotest_lib.server import test from autotest_lib.server import utils from autotest_lib.server.cros.multimedia import remote_facade_factory class graphics_MultipleDisplays(test.test): """Loads multiple WebGL windows on internal and external displays. This test first initializes the extended Chameleon display. It then launches four WebGL windows, two on each display. """ version = 1 WAIT_AFTER_SWITCH = 5 FPS_MEASUREMENT_DURATION = 15 STUCK_FPS_THRESHOLD = 2 MAXIMUM_STUCK_MEASUREMENTS = 5 # Running the HTTP server requires starting Chrome with # init_network_controller set to True. CHROME_KWARGS = {'extension_paths': [constants.AUDIO_TEST_EXTENSION, constants.DISPLAY_TEST_EXTENSION], 'autotest_ext': True, 'init_network_controller': True} # Local WebGL tarballs to populate the webroot. STATIC_CONTENT = ['webgl_aquarium_static.tar.bz2', 'webgl_blob_static.tar.bz2'] # Client directory for the root of the HTTP server CLIENT_TEST_ROOT = \ '/usr/local/autotest/tests/graphics_MultipleDisplays/webroot' # Paths to later convert to URLs WEBGL_AQUARIUM_PATH = \ CLIENT_TEST_ROOT + '/webgl_aquarium_static/aquarium.html' WEBGL_BLOB_PATH = CLIENT_TEST_ROOT + '/webgl_blob_static/blob.html' MEDIA_CONTENT_BASE = ('https://commondatastorage.googleapis.com' '/chromiumos-test-assets-public') H264_URL = MEDIA_CONTENT_BASE + '/Shaka-Dash/1080_60.mp4' VP9_URL = MEDIA_CONTENT_BASE + '/Shaka-Dash/1080_60.webm' # Simple configuration to capture window position, content URL, or local # path. Positioning is either internal or external and left or right half # of the display. As an example, to open the newtab page on the left # half: WindowConfig(True, True, 'chrome://newtab', None). WindowConfig = collections.namedtuple( 'WindowConfig', 'internal_display, snap_left, url, path') WINDOW_CONFIGS = \ {'aquarium+blob': [WindowConfig(True, True, None, WEBGL_AQUARIUM_PATH), WindowConfig(True, False, None, WEBGL_BLOB_PATH), WindowConfig(False, True, None, WEBGL_AQUARIUM_PATH), WindowConfig(False, False, None, WEBGL_BLOB_PATH)], 'aquarium+vp9+blob+h264': [WindowConfig(True, True, None, WEBGL_AQUARIUM_PATH), WindowConfig(True, False, VP9_URL, None), WindowConfig(False, True, None, WEBGL_BLOB_PATH), WindowConfig(False, False, H264_URL, None)]} def _prepare_test_assets(self): """Create a local test bundle and send it to the client. @raise ValueError if the HTTP server does not start. """ # Create a directory to unpack archives. temp_bundle_dir = utils.get_tmp_dir() for static_content in self.STATIC_CONTENT: archive_path = os.path.join(self.bindir, 'files', static_content) with tarfile.open(archive_path, 'r') as tar: tar.extractall(temp_bundle_dir) # Send bundle to client. The extra slash is to send directory contents. self._host.run('mkdir -p {}'.format(self.CLIENT_TEST_ROOT)) self._host.send_file(temp_bundle_dir + '/', self.CLIENT_TEST_ROOT, delete_dest=True) # Start the HTTP server res = self._browser_facade.set_http_server_directories( self.CLIENT_TEST_ROOT) if not res: raise ValueError('HTTP server failed to start.') def _calculate_new_bounds(self, config): """Calculates bounds for 'snapping' to the left or right of a display. @param config: WindowConfig specifying which display and side. @return Dictionary with keys top, left, width, and height for the new window boundaries. """ new_bounds = {'top': 0, 'left': 0, 'width': 0, 'height': 0} display_info = filter( lambda d: d.is_internal == config.internal_display, self._display_facade.get_display_info()) display_info = display_info[0] # Since we are "snapping" windows left and right, set the width to half # and set the height to the full working area. new_bounds['width'] = int(display_info.work_area.width / 2) new_bounds['height'] = display_info.work_area.height # To specify the left or right "snap", first set the left edge to the # display boundary. Note that for the internal display this will be 0. # For the external display it will already include the offset from the # internal display. Finally, if we are positioning to the right half # of the display also add in the width. new_bounds['left'] = display_info.bounds.left if not config.snap_left: new_bounds['left'] = new_bounds['left'] + new_bounds['width'] return new_bounds def _measure_external_display_fps(self, chameleon_port): """Measure the update rate of the external display. @param chameleon_port: ChameleonPort object for recording. @raise ValueError if Chameleon FPS measurements indicate the external display was not changing. """ chameleon_port.start_capturing_video() time.sleep(self.FPS_MEASUREMENT_DURATION) chameleon_port.stop_capturing_video() # FPS information for saving later self._fps_list = chameleon_port.get_captured_fps_list() stuck_fps_list = filter(lambda fps: fps < self.STUCK_FPS_THRESHOLD, self._fps_list) if len(stuck_fps_list) > self.MAXIMUM_STUCK_MEASUREMENTS: msg = 'Too many measurements {} are < {} FPS. GPU hang?'.format( self._fps_list, self.STUCK_FPS_THRESHOLD) raise ValueError(msg) def _setup_windows(self): """Create windows and update their positions. @raise ValueError if the selected subtest is not a valid configuration. @raise ValueError if a window configurations is invalid. """ if self._subtest not in self.WINDOW_CONFIGS: msg = '{} is not a valid subtest. Choices are {}.'.format( self._subtest, self.WINDOW_CONFIGS.keys()) raise ValueError(msg) for window_config in self.WINDOW_CONFIGS[self._subtest]: url = window_config.url if not url: if not window_config.path: msg = 'Path & URL not configured. {}'.format(window_config) raise ValueError(msg) # Convert the locally served content path to a URL. url = self._browser_facade.http_server_url_of( window_config.path) new_bounds = self._calculate_new_bounds(window_config) new_id = self._display_facade.create_window(url) self._display_facade.update_window(new_id, 'normal', new_bounds) time.sleep(self.WAIT_AFTER_SWITCH) def run_once(self, host, subtest, test_duration=60): self._host = host self._subtest = subtest factory = remote_facade_factory.RemoteFacadeFactory(host) self._browser_facade = factory.create_browser_facade() self._browser_facade.start_custom_chrome(self.CHROME_KWARGS) self._display_facade = factory.create_display_facade() self._graphics_facade = factory.create_graphics_facade() logging.info('Preparing local WebGL test assets.') self._prepare_test_assets() chameleon_board = host.chameleon chameleon_board.setup_and_reset(self.outputdir) finder = chameleon_port_finder.ChameleonVideoInputFinder( chameleon_board, self._display_facade) # Snapshot the DUT system logs for any prior GPU hangs self._graphics_facade.graphics_state_checker_initialize() for chameleon_port in finder.iterate_all_ports(): logging.info('Setting Chameleon screen to extended mode.') self._display_facade.set_mirrored(False) time.sleep(self.WAIT_AFTER_SWITCH) logging.info('Launching WebGL windows.') self._setup_windows() logging.info('Measuring the external display update rate.') self._measure_external_display_fps(chameleon_port) logging.info('Running test for {}s.'.format(test_duration)) time.sleep(test_duration) # Raise an error on new GPU hangs self._graphics_facade.graphics_state_checker_finalize() def postprocess_iteration(self): desc = 'Display update rate {}'.format(self._subtest) self.output_perf_value(description=desc, value=self._fps_list, units='FPS', higher_is_better=True, graph=None)