Home | History | Annotate | Download | only in graphics_MultipleDisplays
      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