| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import print_function |
| |
| import logging |
| import os |
| import sys |
| import time |
| from typing import Any, List, Tuple |
| import unittest |
| |
| from gpu_tests import common_browser_args as cba |
| from gpu_tests import common_typing as ct |
| from gpu_tests import gpu_integration_test |
| |
| import gpu_path_util |
| |
| from telemetry.core import exceptions |
| |
| wait_timeout = 60 # seconds |
| |
| harness_script = r""" |
| var domAutomationController = {}; |
| |
| domAutomationController._loaded = false; |
| domAutomationController._succeeded = false; |
| domAutomationController._finished = false; |
| |
| domAutomationController.send = function(msg) { |
| msg = msg.toLowerCase(); |
| if (msg == "loaded") { |
| domAutomationController._loaded = true; |
| } else if (msg == "success") { |
| /* Don't squelch earlier failures! */ |
| if (!domAutomationController._finished) { |
| domAutomationController._succeeded = true; |
| } |
| domAutomationController._finished = true; |
| } else { |
| /* Always record failures. */ |
| domAutomationController._succeeded = false; |
| domAutomationController._finished = true; |
| } |
| } |
| |
| domAutomationController.reset = function() { |
| domAutomationController._succeeded = false; |
| domAutomationController._finished = false; |
| } |
| |
| window.domAutomationController = domAutomationController; |
| console.log("Harness injected."); |
| """ |
| |
| feature_query_script = """ |
| function GetFeatureStatus(feature_name, for_hardware_gpu) { |
| let query_result; |
| const infoView = document.querySelector('info-view'); |
| if (for_hardware_gpu) { |
| query_result = infoView.shadowRoot.querySelector( |
| '.feature-status-for-hardware-gpu-list'); |
| } else { |
| query_result = infoView.shadowRoot.querySelector('.feature-status-list'); |
| } |
| for (let i = 0; i < query_result.childElementCount; i++) { |
| let feature_status = query_result.children[i].textContent.split(': '); |
| if (feature_status.length == 2 && feature_status[0] == feature_name) |
| return feature_status[1]; |
| } |
| return ""; |
| } |
| """ |
| |
| vendor_id_query_script = """ |
| function GetActiveVendorId(for_hardware_gpu) { |
| let div; |
| const infoView = document.querySelector('info-view'); |
| if (for_hardware_gpu) { |
| div = infoView.shadowRoot.querySelector( |
| '.basic-info-for-hardware-gpu-div'); |
| } else { |
| div = infoView.shadowRoot.querySelector('#basic-info'); |
| } |
| const table = div.querySelector('info-view-table'); |
| let trs = table.shadowRoot.querySelectorAll('info-view-table-row'); |
| let vendor_id = 0; |
| // The first four rows are "Initialization time", "In-process GPU", |
| // "Passthrough Command Decoder", and "Sandboxed". |
| for (let i = 4; i < trs.length; i++) { |
| let tds = trs[i].shadowRoot.querySelectorAll('div'); |
| let token = tds[0].textContent.trim(); |
| if (!token.startsWith('GPU')) |
| break; |
| if (i == 4 && token != 'GPU0') |
| break; |
| let gpu_string = tds[1].textContent.trim(); |
| let vendor_info = gpu_string.split(', ')[0].split('= '); |
| if (vendor_info.length != 2 || vendor_info[0] != 'VENDOR') |
| break; |
| let id = parseInt(vendor_info[1]); |
| if (vendor_id == 0) |
| vendor_id = id; |
| if (gpu_string.endsWith('*ACTIVE*')) { |
| vendor_id = id; |
| break; |
| } |
| } |
| return vendor_id; |
| } |
| """ |
| |
| |
| class ContextLostIntegrationTest(gpu_integration_test.GpuIntegrationTest): |
| |
| @classmethod |
| def Name(cls) -> str: |
| return 'context_lost' |
| |
| @classmethod |
| def GenerateBrowserArgs(cls, additional_args: List[str]) -> List[str]: |
| """Adds default arguments to |additional_args|. |
| |
| See the parent class' method documentation for additional information. |
| """ |
| default_args = super(ContextLostIntegrationTest, |
| cls).GenerateBrowserArgs(additional_args) |
| default_args.extend([ |
| # Required to call crashGpuProcess. |
| cba.ENABLE_GPU_BENCHMARKING, |
| # Disable: |
| # Do you want the application "Chromium Helper.app" to accept incoming |
| # network connections? |
| # dialogs on macOS. crbug.com/969559 |
| cba.DISABLE_DEVICE_DISCOVERY_NOTIFICATIONS, |
| ]) |
| return default_args |
| |
| @classmethod |
| def GenerateGpuTests(cls, options: ct.ParsedCmdArgs) -> ct.TestGenerator: |
| # Could not figure out how to prevent yapf from breaking the formatting |
| # below. |
| # yapf: disable |
| tests: Tuple[str, str] = ( |
| ('GpuCrash_GPUProcessCrashesExactlyOncePerVisitToAboutGpuCrash', |
| 'gpu_process_crash.html'), |
| ('ContextLost_WebGPUContextLostFromGPUProcessExit', |
| 'webgpu-context-lost.html?query=kill_after_notification'), |
| ('ContextLost_WebGPUStressRequestDeviceAndRemoveLoop', |
| 'webgpu-stress-request-device-and-remove-loop.html'), |
| ('ContextLost_WebGLContextLostFromGPUProcessExit', |
| 'webgl.html?query=kill_after_notification'), |
| ('ContextLost_WebGLContextLostFromLoseContextExtension', |
| 'webgl.html?query=WEBGL_lose_context'), |
| ('ContextLost_WebGLContextLostFromQuantity', |
| 'webgl.html?query=forced_quantity_loss'), |
| ('ContextLost_WebGLContextLostFromSelectElement', |
| 'webgl_with_select_element.html'), |
| ('ContextLost_WebGLContextLostInHiddenTab', |
| 'webgl.html?query=kill_after_notification'), |
| ('ContextLost_WebGLContextLostOverlyLargeUniform', |
| 'webgl-overly-large-uniform.html'), |
| ('ContextLost_WebGLBlockedAfterJSNavigation', |
| 'webgl-domain-blocking-page1.html'), |
| ('ContextLost_WebGLUnblockedAfterUserInitiatedReload', |
| 'webgl-domain-unblocking.html'), |
| ('GpuNormalTermination_OriginalWebGLNotBlocked', |
| 'webgl-domain-not-blocked.html'), |
| ('GpuNormalTermination_NewWebGLNotBlocked', |
| 'webgl-domain-not-blocked.html'), |
| ('ContextLost_WorkerWebGLRAFAfterGPUCrash', |
| 'worker-webgl-raf-after-gpu-crash.html'), |
| ('ContextLost_WebGL2Blocked', 'webgl2-context-blocked.html'), |
| ('ContextLost_WebGL2UnpackImageHeight', |
| 'webgl2-unpack-image-height.html'), |
| ('ContextLost_MacWebGLMultisamplingHighPowerSwitchLosesContext', |
| 'webgl2-multisampling-high-power-switch-loses-context.html'), |
| ('ContextLost_MacWebGLMultisamplingHighPowerSwitchDoesNotCrash', |
| 'webgl2-multisampling-high-power-switch-does-not-crash.html'), |
| ('ContextLost_MacWebGLPreserveDBHighPowerSwitchLosesContext', |
| 'webgl2-preserve-db-high-power-switch-loses-context.html'), |
| ('GpuCrash_InfoForHardwareGpu', 'simple.html'), |
| ('GpuCrash_InfoForDualHardwareGpus', 'webgl-high-perf.html'), |
| ('ContextLost_WebGPUBlockedAfterJSNavigation', |
| 'webgpu-domain-blocking-page1.html'), |
| ('ContextLost_WebGPUUnblockedAfterUserInitiatedReload', |
| 'webgpu-domain-unblocking.html'), |
| ('GpuNormalTermination_WebGPUNotBlocked', |
| 'webgpu-domain-not-blocked.html')) |
| # yapf: enable |
| |
| for t in tests: |
| yield (t[0], t[1], ['_' + t[0]]) |
| |
| def RunActualGpuTest(self, test_path: str, args: ct.TestArgs) -> None: |
| test_name = args[0] |
| tab = self.tab |
| if not tab.browser.supports_tab_control: |
| self.fail('Browser must support tab control') |
| getattr(self, test_name)(test_path) |
| |
| @classmethod |
| def SetUpProcess(cls) -> None: |
| super(ContextLostIntegrationTest, cls).SetUpProcess() |
| # Most of the tests need this, so add it to the default set of |
| # command line arguments used to launch the browser, to reduce the |
| # number of browser restarts between tests. |
| cls.CustomizeBrowserArgs([cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| cls.StartBrowser() |
| cls.SetStaticServerDirs([gpu_path_util.GPU_DATA_DIR]) |
| |
| def _KillGPUProcess(self, |
| number_of_gpu_process_kills: int, |
| check_crash_count: bool, |
| timeout: int = wait_timeout) -> None: |
| tab = self.tab |
| # Doing the GPU process kill operation cooperatively -- in the |
| # same page's context -- is much more stressful than restarting |
| # the browser every time. |
| for x in range(number_of_gpu_process_kills): |
| expected_kills = x + 1 |
| |
| # Reset the test's state. |
| tab.EvaluateJavaScript('window.domAutomationController.reset()') |
| |
| # If we're running the GPU process crash test, we need the test |
| # to have fully reset before crashing the GPU process. |
| if check_crash_count: |
| tab.WaitForJavaScriptCondition( |
| 'window.domAutomationController._finished', timeout=timeout) |
| |
| # Crash the GPU process. |
| # |
| # This used to create a new tab and navigate it to |
| # chrome://gpucrash, but there was enough unreliability |
| # navigating between these tabs (one of which was created solely |
| # in order to navigate to chrome://gpucrash) that the simpler |
| # solution of provoking the GPU process crash from this renderer |
| # process was chosen. |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| |
| completed = _WaitForPageToFinish(tab, timeout=timeout) |
| |
| if check_crash_count: |
| self._CheckCrashCount(tab, expected_kills) |
| |
| if not completed: |
| self.fail("Test didn't complete (no context lost event?)") |
| if not tab.EvaluateJavaScript( |
| 'window.domAutomationController._succeeded'): |
| self.fail('Test failed (context not restored properly?)') |
| |
| def _CheckCrashCount(self, tab: ct.Tab, expected_kills: int) -> None: |
| system_info = tab.browser.GetSystemInfo() |
| if not system_info: |
| self.fail('Browser must support system info') |
| |
| if not tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail("Test failed (didn't render content properly?)") |
| |
| number_of_crashes = -1 |
| if expected_kills > 0: |
| # To allow time for a gpucrash to complete, wait up to 20s, |
| # polling repeatedly. |
| start_time = time.time() |
| current_time = time.time() |
| while current_time - start_time < 20: |
| system_info = tab.browser.GetSystemInfo() |
| number_of_crashes = \ |
| system_info.gpu.aux_attributes[u'process_crash_count'] |
| if number_of_crashes >= expected_kills: |
| break |
| time.sleep(1) |
| current_time = time.time() |
| |
| # Wait 5 more seconds and re-read process_crash_count, in |
| # attempt to catch latent process crashes. |
| time.sleep(5) |
| system_info = tab.browser.GetSystemInfo() |
| number_of_crashes = \ |
| system_info.gpu.aux_attributes[u'process_crash_count'] |
| |
| if number_of_crashes < expected_kills: |
| self.fail('Timed out waiting for a gpu process crash') |
| elif number_of_crashes != expected_kills: |
| self.fail('Expected %d gpu process crashes; got: %d' % |
| (expected_kills, number_of_crashes)) |
| |
| def _NavigateAndWaitForLoad(self, test_path: str) -> None: |
| url = self.UrlOfStaticFilePath(test_path) |
| tab = self.tab |
| tab.Navigate(url, script_to_evaluate_on_commit=harness_script) |
| tab.action_runner.WaitForJavaScriptCondition( |
| 'window.domAutomationController._loaded') |
| |
| def _GetWebGLFeatureStatus(self, for_hardware_gpu: bool) -> str: |
| tab = self.tab.browser.tabs.New() |
| tab.Navigate('chrome:gpu', |
| script_to_evaluate_on_commit=feature_query_script) |
| tab.WaitForJavaScriptCondition('window.gpuPagePopulated', timeout=10) |
| status = (tab.EvaluateJavaScript('GetFeatureStatus("WebGL", %s)' % |
| ('true' if for_hardware_gpu else 'false'))) |
| tab.Close() |
| return status |
| |
| def _GetActiveVendorId(self, for_hardware_gpu: bool) -> str: |
| tab = self.tab.browser.tabs.New() |
| tab.Navigate('chrome:gpu', |
| script_to_evaluate_on_commit=vendor_id_query_script) |
| tab.WaitForJavaScriptCondition('window.gpuPagePopulated', timeout=10) |
| vid = (tab.EvaluateJavaScript('GetActiveVendorId(%s)' % |
| ('true' if for_hardware_gpu else 'false'))) |
| tab.Close() |
| return vid |
| |
| def _WaitForTabAndCheckCompletion(self, timeout: int = wait_timeout) -> None: |
| tab = self.tab |
| completed = _WaitForPageToFinish(tab, timeout=timeout) |
| if not completed: |
| self.fail("Test didn't complete (no context lost / restored event?)") |
| if not tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail('Test failed (context not restored properly?)') |
| |
| # The browser test runner synthesizes methods with the exact name |
| # given in GenerateGpuTests, so in order to hand-write our tests but |
| # also go through the _RunGpuTest trampoline, the test needs to be |
| # slightly differently named. |
| def _GpuCrash_GPUProcessCrashesExactlyOncePerVisitToAboutGpuCrash( |
| self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| self._KillGPUProcess(2, True) |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGLContextLostFromGPUProcessExit(self, |
| test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| self._KillGPUProcess(1, False) |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGPUContextLostFromGPUProcessExit(self, |
| test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([ |
| '--enable-unsafe-webgpu', |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| self.tab.EvaluateJavaScript( |
| 'chrome.gpuBenchmarking.terminateGpuProcessNormally()') |
| |
| # The gpu startup sometimes takes longer on the bots. |
| # Increasing the timeout for this test as it times out before completion |
| self._WaitForTabAndCheckCompletion(timeout=180) |
| |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGPUStressRequestDeviceAndRemoveLoop(self, test_path: str |
| ) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([ |
| '--enable-unsafe-webgpu', |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| |
| # Test runs for 90 seconds; wait for 120 seconds. |
| self._WaitForTabAndCheckCompletion(timeout=120) |
| |
| def _ContextLost_WebGLContextLostFromLoseContextExtension( |
| self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| url = self.UrlOfStaticFilePath(test_path) |
| tab = self.tab |
| tab.Navigate(url, script_to_evaluate_on_commit=harness_script) |
| tab.action_runner.WaitForJavaScriptCondition( |
| 'window.domAutomationController._finished') |
| |
| def _ContextLost_WebGLContextLostFromQuantity(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| # Try to coerce GC to clean up any contexts not attached to the page. |
| # This method seems unreliable, so the page will also attempt to |
| # force GC through excessive allocations. |
| self.tab.CollectGarbage() |
| self._WaitForTabAndCheckCompletion() |
| |
| def _ContextLost_WebGLContextLostFromSelectElement(self, |
| test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| self._WaitForTabAndCheckCompletion() |
| |
| def _ContextLost_WebGLContextLostInHiddenTab(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| # Test losing a context in a hidden tab. This test passes if the tab |
| # doesn't crash. |
| tab = self.tab |
| dummy_tab = tab.browser.tabs.New() |
| tab.EvaluateJavaScript('loseContextUsingExtension()') |
| tab.Activate() |
| self._WaitForTabAndCheckCompletion() |
| |
| def _ContextLost_WebGLContextLostOverlyLargeUniform(self, |
| test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([ |
| cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS, |
| '--enable-features=DisableArrayBufferSizeLimitsForTesting' |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| # No reason to wait more than 10 seconds for this test to complete. |
| self._WaitForTabAndCheckCompletion(timeout=10) |
| |
| def _ContextLost_WebGLBlockedAfterJSNavigation(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([]) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| # Make sure the tab got a WebGL context. |
| if tab.EvaluateJavaScript('window.domAutomationController._finished'): |
| # This means the test failed for some reason. |
| if tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail('Initial page claimed to succeed early') |
| else: |
| self.fail('Initial page failed to get a WebGL context') |
| # Kill the GPU process. |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| # Wait for the WebGL context to be restored. |
| tab.WaitForJavaScriptCondition('window.restored', timeout=wait_timeout) |
| # Kill the GPU process again. This will cause WebGL to be blocked. |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| # The original tab will navigate to a new page. Wait for it to |
| # finish running its onload handler. |
| # TODO(kbr): figure out when it's OK to evaluate this JavaScript. |
| # Seems racy to do it immediately after crashing the GPU process. |
| tab.WaitForJavaScriptCondition('window.initFinished', timeout=wait_timeout) |
| # Make sure the page failed to get a GL context. |
| if tab.EvaluateJavaScript('window.gotGL'): |
| self.fail( |
| 'Page should have been blocked from getting a new WebGL context') |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGLUnblockedAfterUserInitiatedReload(self, test_path: str |
| ) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([]) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| # Make sure the tab initially got a WebGL context. |
| if not tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail('Tab failed to get an initial WebGL context') |
| # Kill the GPU process once. This won't block WebGL yet. |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| # Wait for the page to receive context loss and restoration events. |
| tab.WaitForJavaScriptCondition('window.contextRestored', |
| timeout=wait_timeout) |
| # Kill the GPU process again. This will cause WebGL to be blocked. |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| # Make sure WebGL is blocked. |
| tab.WaitForJavaScriptCondition( |
| 'window.contextLostReceived', timeout=wait_timeout) |
| if not tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail('WebGL should have been blocked after a second context loss') |
| # Reload the page via Telemetry / DevTools. This is treated as a |
| # user-initiated navigation, so WebGL is unblocked. |
| self._NavigateAndWaitForLoad(test_path) |
| # Ensure WebGL is unblocked. |
| if not tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail( |
| 'WebGL should have been unblocked after a user-initiated navigation') |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _GpuNormalTermination_OriginalWebGLNotBlocked(self, |
| test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([]) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| |
| tab.EvaluateJavaScript( |
| 'chrome.gpuBenchmarking.terminateGpuProcessNormally()') |
| |
| # The webglcontextrestored event on the original canvas should trigger and |
| # report success or failure. |
| self._WaitForTabAndCheckCompletion() |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _GpuNormalTermination_NewWebGLNotBlocked(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([]) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| |
| tab.EvaluateJavaScript( |
| 'chrome.gpuBenchmarking.terminateGpuProcessNormally()') |
| tab.WaitForJavaScriptCondition('window.contextLost', timeout=wait_timeout) |
| tab.EvaluateJavaScript('window.testNewWebGLContext()') |
| |
| self._WaitForTabAndCheckCompletion() |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WorkerWebGLRAFAfterGPUCrash(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([]) |
| self._NavigateAndWaitForLoad(test_path) |
| self._KillGPUProcess(1, False) |
| self._WaitForTabAndCheckCompletion() |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGL2Blocked(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs( |
| ['--gpu-driver-bug-list-test-group=3']) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| tab.EvaluateJavaScript('runTest()') |
| self._WaitForTabAndCheckCompletion() |
| # Attempting to create a WebGL 2.0 context when ES 3.0 is |
| # blocklisted should not cause the GPU process to crash. |
| self._CheckCrashCount(tab, 0) |
| |
| def _ContextLost_WebGL2UnpackImageHeight(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([ |
| cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS, |
| '--enable-features=DisableArrayBufferSizeLimitsForTesting' |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| # No reason to wait more than 10 seconds for this test to complete. |
| self._WaitForTabAndCheckCompletion(timeout=10) |
| |
| def _ContextLost_MacWebGLMultisamplingHighPowerSwitchLosesContext( |
| self, test_path: str) -> None: |
| # Verifies that switching from the low-power to the high-power GPU |
| # on a dual-GPU Mac, while the user has allocated multisampled |
| # renderbuffers via the WebGL 2.0 API, causes the context to be |
| # lost. |
| if not self._IsDualGPUMacLaptop(): |
| logging.info('Skipping test because not running on dual-GPU Mac laptop') |
| self.skipTest('Not running on dual-GPU Mac laptop') |
| # Start with a browser with clean GPU process state. |
| self.RestartBrowserWithArgs([]) |
| # Wait a few seconds for the system to dispatch any GPU switched |
| # notifications. |
| time.sleep(3) |
| self._NavigateAndWaitForLoad(test_path) |
| if not self._IsIntel(self.browser.GetSystemInfo().gpu.devices[0].vendor_id): |
| self.fail('Test did not start up on low-power GPU') |
| tab = self.tab |
| tab.EvaluateJavaScript('runTest()') |
| self._WaitForTabAndCheckCompletion() |
| self._CheckCrashCount(tab, 0) |
| |
| def _ContextLost_MacWebGLMultisamplingHighPowerSwitchDoesNotCrash( |
| self, test_path: str) -> None: |
| # Verifies that switching from the low-power to the high-power GPU |
| # on a dual-GPU Mac, while the user has allocated multisampled |
| # renderbuffers via the WebGL 2.0 API, does not crash. |
| if not self._IsDualGPUMacLaptop(): |
| logging.info('Skipping test because not running on dual-GPU Mac laptop') |
| self.skipTest('Not running on dual-GPU Mac laptop') |
| # Start with a browser with clean GPU process state. |
| self.RestartBrowserWithArgs([]) |
| # Wait a few seconds for the system to dispatch any GPU switched |
| # notifications. |
| time.sleep(3) |
| self._NavigateAndWaitForLoad(test_path) |
| if not self._IsIntel(self.browser.GetSystemInfo().gpu.devices[0].vendor_id): |
| self.fail('Test did not start up on low-power GPU') |
| tab = self.tab |
| tab.EvaluateJavaScript('runTest()') |
| self._WaitForTabAndCheckCompletion() |
| self._CheckCrashCount(tab, 0) |
| |
| def _ContextLost_MacWebGLPreserveDBHighPowerSwitchLosesContext( |
| self, test_path: str) -> None: |
| # Verifies that switching from the low-power to the high-power GPU on a |
| # dual-GPU Mac, when the user specified preserveDrawingBuffer:true, causes |
| # the context to be lost. |
| if not self._IsDualGPUMacLaptop(): |
| logging.info('Skipping test because not running on dual-GPU Mac laptop') |
| self.skipTest('Not running on dual-GPU Mac laptop') |
| # Start with a browser with clean GPU process state. |
| self.RestartBrowserWithArgs([]) |
| # Wait a few seconds for the system to dispatch any GPU switched |
| # notifications. |
| time.sleep(3) |
| self._NavigateAndWaitForLoad(test_path) |
| if not self._IsIntel(self.browser.GetSystemInfo().gpu.devices[0].vendor_id): |
| self.fail('Test did not start up on low-power GPU') |
| tab = self.tab |
| tab.EvaluateJavaScript('runTest()') |
| self._WaitForTabAndCheckCompletion() |
| self._CheckCrashCount(tab, 0) |
| |
| def _GpuCrash_InfoForHardwareGpu(self, test_path: str) -> None: |
| # Ensure that info displayed in chrome:gpu for hardware gpu is correct, |
| # after gpu process crashes three times and falls back to SwiftShader. |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| self._NavigateAndWaitForLoad(test_path) |
| # Check WebGL status at browser startup. |
| webgl_status = self._GetWebGLFeatureStatus(False) |
| if webgl_status != 'Hardware accelerated': |
| self.fail('WebGL should be hardware accelerated initially, but got %s' % |
| webgl_status) |
| webgl_status_for_hardware_gpu = self._GetWebGLFeatureStatus(True) |
| if webgl_status_for_hardware_gpu != '': |
| self.fail('Feature status for hardware gpu should not be displayed ' |
| 'initially') |
| # Check WebGL status after three GPU crashes - fallback to SwiftShader. |
| self._KillGPUProcess(3, True) |
| webgl_status = self._GetWebGLFeatureStatus(False) |
| if webgl_status != 'Software only, hardware acceleration unavailable': |
| self.fail('WebGL should be software only with SwiftShader, but got %s' % |
| webgl_status) |
| webgl_status_for_hardware_gpu = self._GetWebGLFeatureStatus(True) |
| if webgl_status_for_hardware_gpu != 'Hardware accelerated': |
| self.fail('WebGL status for hardware gpu should be "accelerated", ' |
| 'but got %s' % webgl_status_for_hardware_gpu) |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _GpuCrash_InfoForDualHardwareGpus(self, test_path: str) -> None: |
| # Ensure that info displayed in chrome:gpu for hardware gpu is from |
| # the latest active GPU before the crash, after gpu process crashes three |
| # times and falls back to SwiftShader. |
| # Currently the test only works on Mac dual GPU bots. |
| if not self._IsDualGPUMacLaptop(): |
| logging.info('Skipping test because not running on dual-GPU Mac laptop') |
| self.skipTest('Not running on dual-GPU Mac laptop') |
| self.RestartBrowserIfNecessaryWithArgs( |
| [cba.DISABLE_DOMAIN_BLOCKING_FOR_3D_APIS]) |
| active_vendor_id = self._GetActiveVendorId(False) |
| # Load WebGL content and switch to discrete GPU. |
| self._NavigateAndWaitForLoad(test_path) |
| new_active_vendor_id = self._GetActiveVendorId(False) |
| if not active_vendor_id or not new_active_vendor_id: |
| self.fail('Fail to query the active GPU vendor id from about:gpu') |
| # After three GPU crashes, check if the active vendor id for hardware GPU |
| # is the new_active_vendor_id. |
| self._KillGPUProcess(3, True) |
| active_vendor_id_for_hardware_gpu = self._GetActiveVendorId(True) |
| if not active_vendor_id_for_hardware_gpu: |
| self.fail('Fail to query the active GPU vendor id for hardware GPU') |
| if active_vendor_id_for_hardware_gpu != new_active_vendor_id: |
| self.fail('vendor id for hw GPU should be 0x%04x, got 0x%04x' % |
| (new_active_vendor_id, active_vendor_id_for_hardware_gpu)) |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGPUBlockedAfterJSNavigation(self, test_path: str) -> None: |
| self.RestartBrowserIfNecessaryWithArgs([ |
| '--enable-unsafe-webgpu', |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| |
| tab = self.tab |
| if tab.EvaluateJavaScript('window.domAutomationController._finished'): |
| # This means the test failed for some reason. |
| if tab.EvaluateJavaScript('window.domAutomationController._succeeded'): |
| self.fail('Initial page claimed to succeed early') |
| else: |
| self.fail('Initial page failed to get a WebGPU device') |
| |
| # Two times: wait for the page to get a device, and kill the GPU |
| # process. The first time, wait for device lost. The second time, |
| # WebGPU will be blocked. The loop is unrolled for easier debugging. |
| tab.WaitForJavaScriptCondition('window.gotDevice', timeout=wait_timeout) |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| tab.WaitForJavaScriptCondition('window.deviceLost', timeout=wait_timeout) |
| tab.EvaluateJavaScript('proceed = true;') |
| |
| tab.WaitForJavaScriptCondition('window.gotDevice', timeout=wait_timeout) |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| |
| # The original tab will navigate to a new page. Wait for it to |
| # finish running its onload handler. |
| tab.WaitForJavaScriptCondition('window.initFinished', timeout=wait_timeout) |
| |
| ## Make sure the page failed to get a WebGPU adapter. |
| if tab.EvaluateJavaScript('window.gotAdapter'): |
| self.fail( |
| 'Page should have been blocked from getting a new WebGPU device') |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _ContextLost_WebGPUUnblockedAfterUserInitiatedReload( |
| self, test_path: str) -> None: |
| """Tests that user initiated reload unblocks WebGPU crashes. |
| |
| The corresponding test page has two non-failure meaningful state: |
| - Loaded: Page was loaded and a WebGPU device was successfully acquired. |
| - Success: GPU crash occurred and verified that WebGPU is blocked. |
| After the 'Loaded' state, the page waits until a GPU crash occurs and does |
| nothing otherwise. |
| |
| The test runs the test page twice, verifying that the first run can reach |
| 'Success' state while the second run only needs to reach 'Loaded' state to |
| verify that a WebGPU has been unblocked. |
| """ |
| self.RestartBrowserIfNecessaryWithArgs([ |
| '--enable-unsafe-webgpu', |
| ]) |
| # Make sure the tab loaded and initially got a WebGPU device. |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| |
| # Two times: wait for the page to get a device, and kill the GPU |
| # process. The first time, wait for device lost. The second time, |
| # WebGPU will be blocked. The loop is unrolled for easier debugging. |
| tab.WaitForJavaScriptCondition('window.gotDevice', timeout=wait_timeout) |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| tab.WaitForJavaScriptCondition('window.deviceLost', timeout=wait_timeout) |
| tab.EvaluateJavaScript('proceed = true;') |
| |
| tab.WaitForJavaScriptCondition('window.gotDevice', timeout=wait_timeout) |
| tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()') |
| |
| # Verify that WebGPU is now blocked. |
| self._WaitForTabAndCheckCompletion() |
| |
| # Reload the page via Telemetry / DevTools. This is treated as a |
| # user-initiated navigation, so WebGPU is unblocked, and we should be able |
| # to get a new WebGPU device on load. |
| self._NavigateAndWaitForLoad(test_path) |
| tab.WaitForJavaScriptCondition('window.gotDevice', timeout=wait_timeout) |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| def _GpuNormalTermination_WebGPUNotBlocked(self, test_path: str) -> None: |
| """Tests that normal GPU process termination does not block WebGPU. |
| """ |
| self.RestartBrowserIfNecessaryWithArgs([ |
| '--enable-unsafe-webgpu', |
| ]) |
| self._NavigateAndWaitForLoad(test_path) |
| tab = self.tab |
| |
| # Terminate the GPU process. |
| tab.EvaluateJavaScript( |
| 'chrome.gpuBenchmarking.terminateGpuProcessNormally()') |
| |
| # Wait for GPU process to terminate and verify that WebGPU is NOT blocked. |
| self._WaitForTabAndCheckCompletion() |
| self._RestartBrowser('must restart after tests that kill the GPU process') |
| |
| @classmethod |
| def ExpectationsFiles(cls) -> List[str]: |
| return [ |
| os.path.join( |
| os.path.dirname(os.path.abspath(__file__)), 'test_expectations', |
| 'context_lost_expectations.txt') |
| ] |
| |
| |
| def _WaitForPageToFinish(tab, timeout: int = wait_timeout) -> bool: |
| try: |
| tab.WaitForJavaScriptCondition('window.domAutomationController._finished', |
| timeout=timeout) |
| return True |
| except exceptions.TimeoutException: |
| return False |
| |
| |
| def load_tests(loader: unittest.TestLoader, tests: Any, |
| pattern: Any) -> unittest.TestSuite: |
| del loader, tests, pattern # Unused. |
| return gpu_integration_test.LoadAllTestsInModule(sys.modules[__name__]) |