0

Revert "Switch WebGL tests to heartbeat"

This reverts commit d88b939650.

Reason for revert: Causing Fuchsia builders to time out

Original change's description:
> Switch WebGL tests to heartbeat
>
> Switches the WebGL conformance tests to use a heartbeat mechanism
> like the WebGPU conformance tests instead of a fixed 5 minute
> timeout. The 5 minute timeout still exists, but should basically
> never be hit.
>
> The end result of this is that we should fail much faster in
> cases of hung/crashed renderers. Before, we would take 5 minutes
> to detect these, but now we can detect them within 15 seconds.
> This will help normalize shard times and open up opportunities to
> make further improvements such as lowering the shard timeout we
> use.
>
> No noticeable impact to test runtime when tests run normally.
>
> Synopsis of changes under the hood:
> * Moved the WebGL harness JavaScript code into dedicated
>   JavaScript files instead of defining them directly in the Python
>   test harness file.
> * Generalized the WebGPU websocket code for use in both test
>   suites
> * Added Slow expectation support, which is necessary to address
>   slow heartbeats on a few configurations
> * Adjusted the number of parallel jobs on some configurations to
>   ensure heartbeats are sent on time instead of being delayed due
>   to the hardware being hit too hard
>
> Bug: 1354797
> Change-Id: Ie862bd081d9e97947efd10400ab5c7337215124a
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3840756
> Reviewed-by: Yuly Novikov <ynovikov@chromium.org>
> Commit-Queue: Brian Sheedy <bsheedy@chromium.org>
> Reviewed-by: Kenneth Russell <kbr@chromium.org>
> Reviewed-by: Ben Pastene <bpastene@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1054455}

Bug: 1354797
Change-Id: Ia541bca2b4cd8955e6d8e3ea7adeb61f156626d6
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3926272
Auto-Submit: Chong Gu <chonggu@google.com>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Ben Pastene <bpastene@chromium.org>
Commit-Queue: Chong Gu <chonggu@google.com>
Cr-Commit-Position: refs/heads/main@{#1055024}
This commit is contained in:
Chong Gu
2022-10-05 00:05:50 +00:00
committed by Chromium LUCI CQ
parent 6aa2d5ea75
commit 85ae84b891
13 changed files with 198 additions and 548 deletions

@ -166,7 +166,7 @@ wheel: <
version: "version:8.3.1"
# There is currently no Linux arm/arm64 version in CIPD.
not_match_tag <
platform: "linux_arm64"
platform: "linux_aarch64"
>
>
wheel: <
@ -174,7 +174,7 @@ wheel: <
version: "version:4.5.3.56.chromium.4"
# There is currently no Linux arm/arm64 version in CIPD.
not_match_tag <
platform: "linux_arm64"
platform: "linux_aarch64"
>
>
@ -476,7 +476,7 @@ wheel: <
wheel: <
name: "infra/python/wheels/websockets-py3"
version: "version:10.3"
version: "version:10.1"
>
# Used by:
@ -525,7 +525,7 @@ wheel: <
name: "infra/python/wheels/pandas/${vpython_platform}"
version: "version:1.3.2.chromium.1"
not_match_tag: <
platform: "linux_arm64"
platform: "linux_aarch64"
>
>

@ -1,5 +0,0 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
window.onload = function() { window._loaded = true; }

@ -1,137 +0,0 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const HEARTBEAT_THROTTLE_MS = 5000;
class WebSocketWrapper {
constructor() {
this.queued_messages = [];
this.throttle_timer = null;
this.last_heartbeat = null;
this.socket = null;
this._sendDelayedHeartbeat = this._sendDelayedHeartbeat.bind(this);
}
setWebSocket(s) {
this.socket = s;
for (let qm of this.queued_messages) {
s.send(qm);
}
}
_sendMessage(message) {
if (this.socket === null) {
this.queued_messages.push(message);
} else {
this.socket.send(message);
}
}
_sendHeartbeat() {
this._sendMessage('{"type": "TEST_HEARTBEAT"}');
}
_sendDelayedHeartbeat() {
this.throttle_timer = null;
this.last_heartbeat = +new Date();
this._sendHeartbeat();
}
sendHeartbeatThrottled() {
const now = +new Date();
// Heartbeat already scheduled.
if (this.throttle_timer !== null) {
// If we've already passed the point in time where the heartbeat should
// have been sent, cancel it and send it immediately. This helps in cases
// where we've scheduled one, but the test is doing so much work that
// the callback doesn't fire in a reasonable amount of time.
if (this.last_heartbeat !== null &&
now - this.last_heartbeat >= HEARTBEAT_THROTTLE_MS) {
this._clearPendingHeartbeat();
this.last_heartbeat = now;
this._sendHeartbeat();
}
return;
}
// Send a heartbeat immediately.
if (this.last_heartbeat === null ||
now - this.last_heartbeat >= HEARTBEAT_THROTTLE_MS){
this.last_heartbeat = now;
this._sendHeartbeat();
return;
}
// Schedule a heartbeat for the future.
this.throttle_timer = setTimeout(
this._sendDelayedHeartbeat, HEARTBEAT_THROTTLE_MS);
}
_clearPendingHeartbeat() {
if (this.throttle_timer !== null) {
clearTimeout(this.throttle_timer);
this.throttle_timer = null;
}
}
sendTestFinished() {
this._clearPendingHeartbeat();
this._sendMessage('{"type": "TEST_FINISHED"}');
}
}
if (window.parent.wrapper !== undefined) {
const wrapper = window.parent.wrapper;
} else {
const wrapper = new WebSocketWrapper();
window.wrapper = wrapper;
}
function connectWebsocket(port) {
let socket = new WebSocket('ws://127.0.0.1:' + port);
socket.addEventListener('open', () => {
wrapper.setWebSocket(socket);
});
}
var testHarness = {};
testHarness._allTestSucceeded = true;
testHarness._messages = '';
testHarness._failures = 0;
testHarness._finished = false;
testHarness._originalLog = window.console.log;
testHarness.log = function(msg) {
wrapper.sendHeartbeatThrottled();
testHarness._messages += msg + "\n";
testHarness._originalLog.apply(window.console, [msg]);
}
testHarness.reportResults = function(url, success, msg) {
wrapper.sendHeartbeatThrottled();
testHarness._allTestSucceeded = testHarness._allTestSucceeded && !!success;
if(!success) {
testHarness._failures++;
if(msg) {
testHarness.log(msg);
}
}
};
testHarness.notifyFinished = function(url) {
wrapper.sendTestFinished();
testHarness._finished = true;
};
testHarness.navigateToPage = function(src) {
var testFrame = document.getElementById("test-frame");
testFrame.src = src;
};
window.webglTestHarness = testHarness;
window.parent.webglTestHarness = testHarness;
window.console.log = testHarness.log;
window.onerror = function(message, url, line) {
testHarness.reportResults(null, false, message);
testHarness.notifyFinished(null);
};
window.quietMode = function() { return true; }

@ -212,9 +212,6 @@ crbug.com/891953 [ mac ] WebglExtension_OVR_multiview2 [ Failure ]
crbug.com/891953 [ android ] WebglExtension_OVR_multiview2 [ Failure ]
crbug.com/891953 [ linux display-server-wayland ] WebglExtension_OVR_multiview2 [ Failure ]
# Can take a rather long time to load, during which heartbeats aren't sent.
conformance2/sync/sync-webgl-specific.html [ Slow ]
# ========================
# Conformance expectations
# ========================
@ -415,8 +412,6 @@ crbug.com/angleproject/6430 [ mac passthrough angle-metal apple-angle-metal-rend
crbug.com/1298619 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] deqp/functional/gles3/occlusionquery_strict.html [ Failure ]
crbug.com/angleproject/7397 [ mac passthrough angle-metal apple-angle-metal-renderer:-apple-m1 ] conformance2/renderbuffers/invalidate-framebuffer.html [ Failure ]
crbug.com/1363349 [ mac passthrough angle-metal ] conformance/glsl/bugs/complex-glsl-does-not-crash.html [ Slow ]
######################################################################
# Mac failures (mainly OpenGL; some need to be reevaluated on Metal) #
######################################################################

@ -526,8 +526,6 @@ crbug.com/1336736 [ win angle-d3d11 ] conformance/textures/misc/texture-video-tr
crbug.com/982294 [ win debug passthrough nvidia ] conformance/uniforms/uniform-samplers-test.html [ RetryOnFailure ]
crbug.com/1364333 [ win debug angle-vulkan passthrough ] conformance/glsl/bugs/temp-expressions-should-not-crash.html [ Slow ]
crbug.com/1364333 [ win debug angle-vulkan passthrough ] conformance/uniforms/no-over-optimization-on-uniform-array-* [ Slow ]
[ win angle-swiftshader passthrough ] conformance/uniforms/no-over-optimization-on-uniform-array-* [ Slow ]
[ win angle-swiftshader passthrough ] conformance/renderbuffers/framebuffer-object-attachment.html [ Slow ]
@ -573,7 +571,6 @@ crbug.com/angleproject/4846 [ mac angle-metal passthrough ] conformance/uniforms
crbug.com/angleproject/5505 [ mac angle-metal passthrough ] conformance/ogles/GL/acos/acos_001_to_006.html [ Failure ]
crbug.com/angleproject/5505 [ mac angle-metal passthrough ] conformance/ogles/GL/asin/asin_001_to_006.html [ Failure ]
crbug.com/angleproject/6489 [ mac angle-metal passthrough ] conformance/ogles/GL/build/build_009_to_016.html [ Failure ]
crbug.com/1363349 [ mac passthrough angle-metal ] conformance/glsl/bugs/complex-glsl-does-not-crash.html [ Slow ]
# Mac / Passthrough command decoder / Metal / Intel
crbug.com/angleproject/4846 [ mac angle-metal passthrough intel ] conformance/rendering/rendering-stencil-large-viewport.html [ Failure ]

@ -1,141 +0,0 @@
# Copyright 2022 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Code to allow tests to communicate via a websocket server."""
import asyncio
import logging
import sys
import threading
from typing import Optional
import websockets # pylint: disable=import-error
import websockets.server as ws_server # pylint: disable=import-error
WEBSOCKET_PORT_TIMEOUT_SECONDS = 10
WEBSOCKET_SETUP_TIMEOUT_SECONDS = 5
# The client (Chrome) should never be closing the connection. If it does, it's
# indicative of something going wrong like a renderer crash.
ClientClosedConnectionError = websockets.exceptions.ConnectionClosedOK
# Alias for readability and so that users don't have to import asyncio.
WebsocketReceiveMessageTimeoutError = asyncio.TimeoutError
class WebsocketServer():
def __init__(self):
"""Server that abstracts the asyncio calls used under the hood.
Only supports one active connection at a time.
"""
self.server_port = None
self.server_stopper = None
self.connection_stopper = None
self.websocket = None
self.port_set_event = threading.Event()
self.connection_received_event = threading.Event()
self.event_loop = None
self._server_thread = None
def StartServer(self) -> None:
"""Starts the websocket server on a separate thread."""
assert self._server_thread is None, 'Server already running'
self._server_thread = _ServerThread(self)
self._server_thread.daemon = True
self._server_thread.start()
got_port = self.port_set_event.wait(WEBSOCKET_PORT_TIMEOUT_SECONDS)
if not got_port:
raise RuntimeError('Websocket server did not provide a port')
# Through some existing Telemetry magic, we don't need to set up port
# forwarding for remote platforms (ChromeOS/Android). We can provide the
# actual server port on these platforms and the page can connect just fine.
def ClearCurrentConnection(self) -> None:
if self.connection_stopper:
self.connection_stopper.cancel()
try:
self.connection_stopper.exception()
except asyncio.CancelledError:
pass
self.connection_stopper = None
self.websocket = None
self.connection_received_event.clear()
def WaitForConnection(self, timeout: Optional[int] = None) -> None:
if self.websocket:
return
timeout = timeout or WEBSOCKET_SETUP_TIMEOUT_SECONDS
self.connection_received_event.wait(timeout)
if not self.websocket:
raise RuntimeError('Websocket connection was not established')
def StopServer(self) -> None:
self.ClearCurrentConnection()
if self.server_stopper:
self.server_stopper.cancel()
try:
self.server_stopper.exception()
except asyncio.CancelledError:
pass
self.server_stopper = None
self.server_port = None
self._server_thread.join(5)
if self._server_thread.is_alive():
logging.error(
'Websocket server did not shut down properly - this might be '
'indicative of an issue in the test harness')
def Send(self, message: str) -> None:
asyncio.run_coroutine_threadsafe(self.websocket.send(message),
self.event_loop)
def Receive(self, timeout: int) -> str:
future = asyncio.run_coroutine_threadsafe(
asyncio.wait_for(self.websocket.recv(), timeout), self.event_loop)
try:
return future.result()
except asyncio.exceptions.TimeoutError as e:
raise WebsocketReceiveMessageTimeoutError(
'Timed out after %d seconds waiting for websocket message' %
timeout) from e
class _ServerThread(threading.Thread):
def __init__(self, server_instance: WebsocketServer, *args, **kwargs):
super().__init__(*args, **kwargs)
self._server_instance = server_instance
def run(self) -> None:
try:
asyncio.run(StartWebsocketServer(self._server_instance))
except asyncio.CancelledError:
pass
except Exception as e: # pylint: disable=broad-except
sys.stdout.write('Server thread had an exception: %s\n' % e)
async def StartWebsocketServer(server_instance: WebsocketServer) -> None:
async def HandleWebsocketConnection(
websocket: ws_server.WebSocketServerProtocol) -> None:
# We only allow one active connection - if there are multiple, something is
# wrong.
assert server_instance.connection_stopper is None
assert server_instance.websocket is None
server_instance.connection_stopper = asyncio.Future()
# Keep our own reference in case the server clears its reference before the
# await finishes.
connection_stopper = server_instance.connection_stopper
server_instance.websocket = websocket
server_instance.connection_received_event.set()
await connection_stopper
async with websockets.serve(HandleWebsocketConnection, '127.0.0.1',
0) as server:
server_instance.event_loop = asyncio.get_running_loop()
server_instance.server_port = server.sockets[0].getsockname()[1]
server_instance.port_set_event.set()
server_instance.server_stopper = asyncio.Future()
server_stopper = server_instance.server_stopper
await server_stopper

@ -3,11 +3,9 @@
# found in the LICENSE file.
import logging
import json
import os
import re
import sys
import time
from typing import Any, List, Optional, Set, Tuple
import unittest
@ -16,29 +14,54 @@ from gpu_tests import common_typing as ct
from gpu_tests import gpu_helper
from gpu_tests import gpu_integration_test
from gpu_tests import webgl_test_util
from gpu_tests.util import websocket_server
import gpu_path_util
from telemetry.internal.platform import gpu_info as telemetry_gpu_info
JAVASCRIPT_DIR = os.path.join(gpu_path_util.GPU_DIR, 'gpu_tests', 'javascript')
conformance_harness_script = r"""
var testHarness = {};
testHarness._allTestSucceeded = true;
testHarness._messages = '';
testHarness._failures = 0;
testHarness._finished = false;
testHarness._originalLog = window.console.log;
WEBSOCKET_JAVASCRIPT_TIMEOUT_S = 30
HEARTBEAT_TIMEOUT_S = 15
ASAN_MULTIPLIER = 2
SLOW_MULTIPLIER = 4
testHarness.log = function(msg) {
testHarness._messages += msg + "\n";
testHarness._originalLog.apply(window.console, [msg]);
}
# Non-standard timeouts that can't be handled by a Slow expectation, likely due
# to being particularly long or not specific to a configuration. Try to use
# expectations first.
NON_STANDARD_HEARTBEAT_TIMEOUTS = {}
testHarness.reportResults = function(url, success, msg) {
testHarness._allTestSucceeded = testHarness._allTestSucceeded && !!success;
if(!success) {
testHarness._failures++;
if(msg) {
testHarness.log(msg);
}
}
};
testHarness.notifyFinished = function(url) {
testHarness._finished = true;
};
testHarness.navigateToPage = function(src) {
var testFrame = document.getElementById("test-frame");
testFrame.src = src;
};
# Non-standard timeouts for executing the JavaScript to establish the websocket
# connection. A test being in here implies that it starts doing a huge amount
# of work in JavaScript immediately, preventing execution of JavaScript via
# devtools as well.
NON_STANDARD_WEBSOCKET_JAVASCRIPT_TIMEOUTS = {}
window.webglTestHarness = testHarness;
window.parent.webglTestHarness = testHarness;
window.console.log = testHarness.log;
window.onerror = function(message, url, line) {
testHarness.reportResults(null, false, message);
testHarness.notifyFinished(null);
};
window.quietMode = function() { return true; }
"""
extension_harness_additional_script = r"""
window.onload = function() { window._loaded = true; }
"""
# cmp no longer exists in Python 3
def cmp(a: Any, b: Any) -> int:
@ -72,12 +95,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
_verified_flags = False
_original_environ = None
# Scripts read from file during process start up.
_conformance_harness_script = None
_extension_harness_additional_script = None
websocket_server = None
@classmethod
def Name(cls) -> str:
return 'webgl_conformance'
@ -98,13 +115,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
return {
# crbug.com/1347970.
'conformance/textures/misc/texture-video-transparent.html',
# Specifically when using Metal, this test can be rather slow. When run
# in parallel, even a minute is not long enough for it to reliably run,
# as it does not properly send heartbeats (possibly due to a large
# amount of work being done). Instead of increasing the heartbeat
# timeout further, run it serially. Can potentially be removed depending
# on the response to crbug.com/1363349.
'conformance/glsl/bugs/complex-glsl-does-not-crash.html',
}
@classmethod
@ -126,17 +136,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
@classmethod
def _SetClassVariablesFromOptions(cls, options: ct.ParsedCmdArgs) -> None:
cls._webgl_version = int(options.webgl_conformance_version.split('.')[0])
if not cls._conformance_harness_script:
with open(
os.path.join(JAVASCRIPT_DIR,
'webgl_conformance_harness_script.js')) as f:
cls._conformance_harness_script = f.read()
if not cls._extension_harness_additional_script:
with open(
os.path.join(
JAVASCRIPT_DIR,
'webgl_conformance_extension_harness_additional_script.js')) as f:
cls._extension_harness_additional_script = f.read()
@classmethod
def GenerateGpuTests(cls, options: ct.ParsedCmdArgs) -> ct.TestGenerator:
@ -343,86 +342,10 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
self._verified_flags = True
url = self.UrlOfStaticFilePath(test_path)
self.tab.Navigate(url, script_to_evaluate_on_commit=harness_script)
self.tab.action_runner.EvaluateJavaScript(
'connectWebsocket("%d")' %
WebGLConformanceIntegrationTest.websocket_server.server_port,
timeout=self._GetWebsocketJavaScriptTimeout())
WebGLConformanceIntegrationTest.websocket_server.WaitForConnection()
def _HandleMessageLoop(self, test_timeout: float) -> None:
start_time = time.time()
try:
while True:
response = WebGLConformanceIntegrationTest.websocket_server.Receive(
self._GetHeartbeatTimeout())
response = json.loads(response)
response_type = response['type']
if time.time() - start_time > test_timeout:
raise RuntimeError(
'Hit %.3f second global timeout, but page continued to send '
'messages over the websocket, i.e. was not due to a renderer '
'crash.' % test_timeout)
if response_type == 'TEST_HEARTBEAT':
continue
if response_type == 'TEST_FINISHED':
break
raise RuntimeError('Received unknown message type %s' % response_type)
except websocket_server.WebsocketReceiveMessageTimeoutError:
logging.error(
'Timed out waiting for websocket message, checking for hung renderer')
# Telemetry has some code to automatically crash the renderer and GPU
# processes if it thinks that the renderer is hung. So, execute some
# trivial JavaScript now to hit that code if we got the timeout because of
# a hung renderer. If we do detect a hung renderer, this will raise
# another exception and prevent the following line about the renderer not
# being hung from running.
self.tab.action_runner.EvaluateJavaScript('let somevar = undefined;',
timeout=5)
logging.error('Timeout does *not* appear to be due to a hung renderer')
raise
except websocket_server.ClientClosedConnectionError as e:
raise RuntimeError(
'Detected closed websocket - likely caused by a renderer '
'crash') from e
finally:
WebGLConformanceIntegrationTest.websocket_server.ClearCurrentConnection()
def _GetWebsocketJavaScriptTimeout(self) -> int:
# Most tests should be able to run JavaScript immediately after page load.
# However, some tests will do so much work that we're unable to actually
# run the JavaScript for quite a while.
return int(
NON_STANDARD_WEBSOCKET_JAVASCRIPT_TIMEOUTS.get(
self.shortName(), WEBSOCKET_JAVASCRIPT_TIMEOUT_S) *
self._GetTimeoutMultiplier())
def _GetHeartbeatTimeout(self) -> int:
return int(
NON_STANDARD_HEARTBEAT_TIMEOUTS.get(self.shortName(),
HEARTBEAT_TIMEOUT_S) *
self._GetTimeoutMultiplier())
def _GetTimeoutMultiplier(self) -> float:
# Parallel jobs increase load and can slow down test execution, so scale
# based on the number of jobs. Target 2x increase with 4 jobs.
multiplier = 1 + (self.child.jobs - 1) / 3.0
if self.is_asan:
multiplier *= ASAN_MULTIPLIER
if self._IsSlowTest():
multiplier *= SLOW_MULTIPLIER
return multiplier
def _IsSlowTest(self) -> bool:
# We access the expectations directly instead of using
# self.GetExpectationsForTest since we need the raw results, but that method
# only returns the parsed results and whether the test should be retried.
expectation = self.child.expectations.expectations_for(self.shortName())
return 'Slow' in expectation.raw_results
def _CheckTestCompletion(self) -> None:
self._HandleMessageLoop(self._GetTestTimeout())
self.tab.action_runner.WaitForJavaScriptCondition(
'webglTestHarness._finished', timeout=self._GetTestTimeout())
if self._crash_count != self.browser.GetSystemInfo().gpu \
.aux_attributes['process_crash_count']:
self.fail('GPU process crashed during test.\n' +
@ -431,12 +354,12 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
self.fail(self._WebGLTestMessages(self.tab))
def _RunConformanceTest(self, test_path: str, _: WebGLTestArgs) -> None:
self._NavigateTo(test_path, self._conformance_harness_script)
self._NavigateTo(test_path, conformance_harness_script)
self._CheckTestCompletion()
def _RunExtensionCoverageTest(self, test_path: str,
test_args: WebGLTestArgs) -> None:
self._NavigateTo(test_path, self._GetExtensionHarnessScript())
self._NavigateTo(test_path, _GetExtensionHarnessScript())
self.tab.action_runner.WaitForJavaScriptCondition(
'window._loaded', timeout=self._GetTestTimeout())
context_type = 'webgl2' if test_args.webgl_version == 2 else 'webgl'
@ -451,7 +374,7 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
self._CheckTestCompletion()
def _RunExtensionTest(self, test_path: str, test_args: WebGLTestArgs) -> None:
self._NavigateTo(test_path, self._GetExtensionHarnessScript())
self._NavigateTo(test_path, _GetExtensionHarnessScript())
self.tab.action_runner.WaitForJavaScriptCondition(
'window._loaded', timeout=self._GetTestTimeout())
context_type = 'webgl2' if test_args.webgl_version == 2 else 'webgl'
@ -468,12 +391,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
timeout *= 2
return timeout
def _GetExtensionHarnessScript(self) -> str:
assert self._conformance_harness_script is not None
assert self._extension_harness_additional_script is not None
return (self._conformance_harness_script +
self._extension_harness_additional_script)
@classmethod
def GenerateBrowserArgs(cls, additional_args: List[str]) -> List[str]:
"""Adds default arguments to |additional_args|.
@ -539,11 +456,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
@classmethod
def SetUpProcess(cls) -> None:
super(WebGLConformanceIntegrationTest, cls).SetUpProcess()
# Logging every time a connection is opened/closed is spammy, so decrease
# the default log level.
logging.getLogger('websockets.server').setLevel(logging.WARNING)
cls.websocket_server = websocket_server.WebsocketServer()
cls.websocket_server.StartServer()
cls.CustomizeBrowserArgs([])
cls.StartBrowser()
# By setting multiple server directories, the root of the server
@ -556,12 +468,6 @@ class WebGLConformanceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
webgl_test_util.extensions_relpath)
])
@classmethod
def TearDownProcess(cls) -> None:
cls.websocket_server.StopServer()
cls.websocket_server = None
super(WebGLConformanceIntegrationTest, cls).TearDownProcess()
# Helper functions.
@staticmethod
@ -708,6 +614,10 @@ def _GetGPUInfoErrorString(gpu_info: telemetry_gpu_info.GPUInfo) -> str:
return error_str
def _GetExtensionHarnessScript() -> str:
return conformance_harness_script + extension_harness_additional_script
def load_tests(loader: unittest.TestLoader, tests: Any,
pattern: Any) -> unittest.TestSuite:
del loader, tests, pattern # Unused.

@ -2,17 +2,22 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import asyncio
import fnmatch
import json
import logging
import os
import sys
import threading
import time
from typing import Any, Dict, List, Set
import unittest
import websockets # pylint:disable=import-error
import websockets.server as ws_server # pylint: disable=import-error
from gpu_tests import common_typing as ct
from gpu_tests import gpu_integration_test
from gpu_tests.util import websocket_server
import gpu_path_util
@ -59,6 +64,37 @@ class WebGpuTestResult():
self.log_pieces = []
async def StartWebsocketServer() -> None:
async def HandleWebsocketConnection(
websocket: ws_server.WebSocketServerProtocol) -> None:
# We only allow one active connection - if there are multiple, something is
# wrong.
assert WebGpuCtsIntegrationTest.connection_stopper is None
assert WebGpuCtsIntegrationTest.websocket is None
WebGpuCtsIntegrationTest.connection_stopper = asyncio.Future()
WebGpuCtsIntegrationTest.websocket = websocket
WebGpuCtsIntegrationTest.connection_received_event.set()
await WebGpuCtsIntegrationTest.connection_stopper
async with websockets.serve(HandleWebsocketConnection, '127.0.0.1',
0) as server:
WebGpuCtsIntegrationTest.event_loop = asyncio.get_running_loop()
WebGpuCtsIntegrationTest.server_port = server.sockets[0].getsockname()[1]
WebGpuCtsIntegrationTest.port_set_event.set()
WebGpuCtsIntegrationTest.server_stopper = asyncio.Future()
await WebGpuCtsIntegrationTest.server_stopper
class ServerThread(threading.Thread):
def run(self) -> None:
try:
asyncio.run(StartWebsocketServer())
except asyncio.CancelledError:
pass
except Exception as e: # pylint:disable=broad-except
sys.stdout.write('Server thread had exception: %s\n' % e)
class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
# Whether the test page has already been loaded. Caching this state here is
# faster than checking the URL every time, and given how fast these tests are,
@ -77,7 +113,14 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
total_tests_run = 0
websocket_server = None
server_stopper = None
connection_stopper = None
server_port = None
websocket = None
port_set_event = None
connection_received_event = None
event_loop = None
_server_thread = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -127,12 +170,25 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
cls._page_loaded = False
super(WebGpuCtsIntegrationTest, cls).StartBrowser()
@classmethod
def SetUpWebsocketServer(cls) -> None:
cls.port_set_event = threading.Event()
cls.connection_received_event = threading.Event()
cls._server_thread = ServerThread()
# Mark as a daemon so that the harness does not hang when shutting down if
# the thread fails to shut down properly.
cls._server_thread.daemon = True
cls._server_thread.start()
got_port = WebGpuCtsIntegrationTest.port_set_event.wait(
WEBSOCKET_PORT_TIMEOUT_SECONDS)
if not got_port:
raise RuntimeError('Server did not provide a port.')
@classmethod
def SetUpProcess(cls) -> None:
super(WebGpuCtsIntegrationTest, cls).SetUpProcess()
cls.websocket_server = websocket_server.WebsocketServer()
cls.websocket_server.StartServer()
cls.SetUpWebsocketServer()
browser_args = [
'--enable-unsafe-webgpu',
'--disable-dawn-features=disallow_unsafe_apis',
@ -159,10 +215,34 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
os.path.join(cls._build_dir, 'gen', 'third_party', 'dawn'),
])
@classmethod
def TearDownWebsocketServer(cls) -> None:
if cls.connection_stopper:
cls.connection_stopper.cancel()
try:
cls.connection_stopper.exception()
except asyncio.CancelledError:
pass
if cls.server_stopper:
cls.server_stopper.cancel()
try:
cls.server_stopper.exception()
except asyncio.CancelledError:
pass
cls.server_stopper = None
cls.connection_stopper = None
cls.server_port = None
cls.websocket = None
cls._server_thread.join(5)
if cls._server_thread.is_alive():
logging.error(
'WebSocket server did not shut down properly - this might be '
'indicative of an issue in the test harness')
@classmethod
def TearDownProcess(cls) -> None:
cls.websocket_server.StopServer()
cls.websocket_server = None
cls.TearDownWebsocketServer()
super(WebGpuCtsIntegrationTest, cls).TearDownProcess()
@classmethod
@ -221,12 +301,13 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
try:
first_load = self._NavigateIfNecessary(test_path)
WebGpuCtsIntegrationTest.websocket_server.Send(
json.dumps({
'q': self._query,
'w': self._run_in_worker
}))
result = self.HandleMessageLoop(first_load)
asyncio.run_coroutine_threadsafe(
WebGpuCtsIntegrationTest.websocket.send(
json.dumps({
'q': self._query,
'w': self._run_in_worker
})), WebGpuCtsIntegrationTest.event_loop)
result = self.HandleMessageLoop(first_load=first_load)
log_str = ''.join(result.log_pieces)
status = result.status
@ -235,7 +316,7 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
log_str)
elif status == 'fail':
self.fail(log_str)
except websocket_server.ClientClosedConnectionError as e:
except websockets.exceptions.ConnectionClosedOK as e:
raise RuntimeError(
'Detected closed websocket - likely caused by renderer crash') from e
except WebGpuMessageTimeoutError as e:
@ -320,7 +401,10 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
while True:
timeout = step_timeout * browser_timeout_multiplier
try:
response = WebGpuCtsIntegrationTest.websocket_server.Receive(timeout)
future = asyncio.run_coroutine_threadsafe(
asyncio.wait_for(WebGpuCtsIntegrationTest.websocket.recv(),
timeout), WebGpuCtsIntegrationTest.event_loop)
response = future.result()
response = json.loads(response)
response_type = response['type']
@ -363,7 +447,7 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
else:
raise WebGpuMessageProtocolError('Received unknown message type %s' %
response_type)
except websocket_server.WebsocketReceiveMessageTimeoutError as e:
except asyncio.TimeoutError as e:
self.HandleDurationTagOnFailure(message_state, global_timeout)
raise WebGpuMessageTimeoutError(
'Timed out waiting %.3f seconds for a message. Message state: %s' %
@ -388,18 +472,32 @@ class WebGpuCtsIntegrationTest(gpu_integration_test.GpuIntegrationTest):
and JAVASCRIPT_DURATION not in self.additionalTags):
self.additionalTags[JAVASCRIPT_DURATION] = '%.9fs' % test_timeout
@classmethod
def CleanUpExistingWebsocket(cls) -> None:
if cls.connection_stopper:
cls.connection_stopper.cancel()
try:
cls.connection_stopper.exception()
except asyncio.CancelledError:
pass
cls.connection_stopper = None
cls.websocket = None
cls.connection_received_event.clear()
def _NavigateIfNecessary(self, path: str) -> bool:
if WebGpuCtsIntegrationTest._page_loaded:
return False
WebGpuCtsIntegrationTest.websocket_server.ClearCurrentConnection()
WebGpuCtsIntegrationTest.CleanUpExistingWebsocket()
url = self.UrlOfStaticFilePath(path)
self.tab.Navigate(url)
self.tab.action_runner.WaitForJavaScriptCondition(
'window.setupWebsocket != undefined')
self.tab.action_runner.ExecuteJavaScript(
'window.setupWebsocket("%s")' %
WebGpuCtsIntegrationTest.websocket_server.server_port)
WebGpuCtsIntegrationTest.websocket_server.WaitForConnection()
'window.setupWebsocket("%s")' % WebGpuCtsIntegrationTest.server_port)
WebGpuCtsIntegrationTest.connection_received_event.wait(
WEBSOCKET_SETUP_TIMEOUT_SECONDS)
if not WebGpuCtsIntegrationTest.websocket:
raise RuntimeError('Websocket connection was not established.')
WebGpuCtsIntegrationTest._page_loaded = True
return True

@ -128,24 +128,6 @@ def GPUExpectedDeviceId(test_config, _, tester_config):
return retval
def _GetGpusFromTestConfig(test_config):
"""Generates all GPU dimension strings from a test config.
Args:
test_config: A dict containing a configuration for a specific test on a
specific builder.
"""
dimensions = test_config.get('swarming', {}).get('dimension_sets', [])
assert dimensions
for d in dimensions:
# Split up multiple GPU/driver combinations if the swarming OR operator is
# being used.
if 'gpu' in d:
gpus = d['gpu'].split('|')
for gpu in gpus:
yield gpu
def GPUParallelJobs(test_config, _, tester_config):
"""Substitutes the correct number of jobs for GPU tests.
@ -165,26 +147,19 @@ def GPUParallelJobs(test_config, _, tester_config):
# Return --jobs=1 for Windows Intel bots running the WebGPU CTS
# These bots can't handle parallel tests. See crbug.com/1353938.
# The load can also negatively impact WebGL tests, so reduce the number of
# jobs there.
# TODO(crbug.com/1349828): Try removing the Windows special casing once we
# swap which machines we're using.
is_webgpu_cts = test_name.startswith('webgpu_cts') or test_config.get(
'telemetry_test_name') == 'webgpu_cts'
is_webgl_cts = 'webgl_conformance' in test_name or test_config.get(
'telemetry_test_name') == 'webgl_conformance'
if os_type == 'win' and (is_webgl_cts or is_webgpu_cts):
for gpu in _GetGpusFromTestConfig(test_config):
if gpu.startswith('8086'):
if is_webgpu_cts:
return ['--jobs=1']
return ['--jobs=2']
# Similarly, the NVIDIA Macbooks are quite old and slow, so reduce the number
# of jobs there as well.
if os_type == 'mac' and is_webgl_cts:
for gpu in _GetGpusFromTestConfig(test_config):
if gpu.startswith('10de'):
return ['--jobs=3']
if os_type == 'win' and is_webgpu_cts:
dimensions = test_config.get('swarming', {}).get('dimension_sets', [])
assert dimensions
for d in dimensions:
# Split up multiple GPU/driver combinations if the swarming OR operator is
# being used.
if 'gpu' in d:
gpus = d['gpu'].split('|')
for gpu in gpus:
if gpu.startswith('8086'):
return ['--jobs=1']
if os_type in ['lacros', 'linux', 'mac', 'win']:
return ['--jobs=4']

@ -178,9 +178,9 @@ class GPUParallelJobs(unittest.TestCase):
def testWebGPUCTSWindowsIntelSerialJobs(self):
intel_config = CreateConfigWithGpus(['8086:device1-driver'])
amd_config = CreateConfigWithGpus(['1002:device1-driver'])
nvidia_config = CreateConfigWithGpus(['10de:device1-driver'])
for gpu_config in [intel_config, amd_config]:
for gpu_config in [intel_config, nvidia_config]:
for name, telemetry_test_name in [('webgpu_cts', None),
(None, 'webgpu_cts')]:
is_intel = intel_config == gpu_config
@ -197,48 +197,6 @@ class GPUParallelJobs(unittest.TestCase):
else:
self.assertEqual(retval, ['--jobs=4'])
def testWebGLWindowsIntelParallelJobs(self):
intel_config = CreateConfigWithGpus(['8086:device1-driver'])
amd_config = CreateConfigWithGpus(['1002:device1-driver'])
for gpu_config in [intel_config, amd_config]:
for name, telemetry_test_name in [('webgl_conformance', None),
(None, 'webgl_conformance')]:
is_intel = intel_config == gpu_config
c = gpu_config.copy()
if name:
c['name'] = name
if telemetry_test_name:
c['telemetry_test_name'] = telemetry_test_name
for os_type in ['lacros', 'linux', 'mac', 'win']:
retval = magic_substitutions.GPUParallelJobs(c, None,
{'os_type': os_type})
if is_intel and os_type == 'win':
self.assertEqual(retval, ['--jobs=2'])
else:
self.assertEqual(retval, ['--jobs=4'])
def testWebGLMacNvidiaParallelJobs(self):
amd_config = CreateConfigWithGpus(['1002:device1-driver'])
nvidia_config = CreateConfigWithGpus(['10de:device1-driver'])
for gpu_config in [nvidia_config, amd_config]:
for name, telemetry_test_name in [('webgl_conformance', None),
(None, 'webgl_conformance')]:
is_nvidia = gpu_config == nvidia_config
c = gpu_config.copy()
if name:
c['name'] = name
if telemetry_test_name:
c['telemetry_test_name'] = telemetry_test_name
for os_type in ['lacros', 'linux', 'mac', 'win']:
retval = magic_substitutions.GPUParallelJobs(c, None,
{'os_type': os_type})
if is_nvidia and os_type == 'mac':
self.assertEqual(retval, ['--jobs=3'])
else:
self.assertEqual(retval, ['--jobs=4'])
def CreateConfigWithDeviceTypes(device_types):
dimension_sets = []

@ -1278,7 +1278,7 @@
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--webgl-conformance-version=2.0.1",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl2_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -1317,7 +1317,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -1356,7 +1356,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d9 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -1394,7 +1394,7 @@
"-v",
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-angle=vulkan --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {

@ -16609,7 +16609,7 @@
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=gl --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--webgl-conformance-version=2.0.1",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl2_conformance_tests_output.json",
"--jobs=3"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -16651,7 +16651,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=gl --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=3"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -16693,7 +16693,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=swiftshader --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--test-filter=conformance/rendering/gl-drawelements.html",
"--jobs=3"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -19538,7 +19538,7 @@
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--webgl-conformance-version=2.0.1",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl2_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -19577,7 +19577,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -19616,7 +19616,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d9 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -19654,7 +19654,7 @@
"-v",
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-angle=vulkan --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {

@ -665,7 +665,7 @@
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--webgl-conformance-version=2.0.1",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl2_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -744,7 +744,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d11 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -823,7 +823,7 @@
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-gl=angle --use-angle=d3d9 --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--read-abbreviated-json-results-from=../../content/test/data/gpu/webgl_conformance_tests_output.json",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {
@ -900,7 +900,7 @@
"-v",
"--stable-jobs",
"--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc --use-angle=vulkan --use-cmd-decoder=passthrough --force_high_performance_gpu",
"--jobs=2"
"--jobs=4"
],
"isolate_name": "telemetry_gpu_integration_test",
"merge": {