• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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