aboutsummaryrefslogtreecommitdiff
path: root/server/site_tests/graphics_MultipleDisplays/graphics_MultipleDisplays.py
blob: 21a4d5890db8ec1eb6aebc5d562a979518713299 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# 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)