blob: 21a4d5890db8ec1eb6aebc5d562a979518713299 [file] [log] [blame]
# 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)