blob: 49ad5381c028e4e59ed29c224db59d97c0c297d5 [file] [log] [blame]
Ilja H. Friedelb755ce832021-10-02 12:51:16 -07001# Lint as: python2, python3
Ed Bakerd1bde4e2018-11-06 12:34:10 -08002# 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.
Ed Bakerfaa25dc2019-02-08 11:21:41 -080092 self._host.run('mkdir -p {}'.format(self.CLIENT_TEST_ROOT))
Ed Bakerd1bde4e2018-11-06 12:34:10 -080093 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}
Ilja H. Friedel3c0ef5d2021-08-20 17:18:50 -0700111 display_info = [d for d in self._display_facade.get_display_info() if d.is_internal == config.internal_display]
Ed Bakerd1bde4e2018-11-06 12:34:10 -0800112 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
Ilja H. Friedel3c0ef5d2021-08-20 17:18:50 -0700145 stuck_fps_list = [fps for fps in self._fps_list if fps < self.STUCK_FPS_THRESHOLD]
Ed Bakerd1bde4e2018-11-06 12:34:10 -0800146 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(
Ilja H. Friedel3c0ef5d2021-08-20 17:18:50 -0700160 self._subtest, list(self.WINDOW_CONFIGS.keys()))
Ed Bakerd1bde4e2018-11-06 12:34:10 -0800161 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)