| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "components/viz/host/gpu_client.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/browser/renderer_host/spare_render_process_host_manager_impl.h" |
| #include "content/browser/webui/web_ui_data_source_impl.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui_controller.h" |
| #include "content/public/browser/web_ui_controller_factory.h" |
| #include "content/public/browser/webui_config.h" |
| #include "content/public/browser/webui_config_map.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_utils.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_content_browser_client.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/shell/browser/shell.h" |
| #include "gpu/config/gpu_finch_features.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| std::string GetForceCompositeScript() { |
| return R"( |
| new Promise(resolve => { |
| // Force a visual update to ensure Viz composites a frame. |
| const div = document.createElement('div'); |
| div.style.width = '100px'; |
| div.style.height = '100px'; |
| div.style.backgroundColor = 'green'; |
| document.body.appendChild(div); |
| |
| // Wait for Viz to composite the frames. |
| requestAnimationFrame(() => { |
| requestAnimationFrame(() => { |
| resolve(true); |
| }); |
| }); |
| }); |
| )"; |
| } |
| |
| } // namespace |
| |
| class InitialGpuChannelBrowserTest : public ContentBrowserTest { |
| public: |
| InitialGpuChannelBrowserTest() { |
| feature_list_.InitAndEnableFeatureWithParameters( |
| features::kSendGPUChannelEarly, |
| {{"for_topchrome_webui_only", "false"}}); |
| } |
| |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelBrowserTest, |
| StandardRendererUsesEarlyChannel) { |
| // Navigate to a page using a standard (non-spare) renderer process. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| // Verify that the renderer is able to use the early channel to submit frames. |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| frame_observer.SetWaitForNextFrame(); |
| |
| // Verify that rendering works. |
| EXPECT_EQ(true, EvalJs(shell(), GetForceCompositeScript())); |
| |
| // Wait for Viz to composite the frames. |
| frame_observer.WaitForNextFrameSubmission(); |
| |
| content::FetchHistogramsFromChildProcesses(); |
| |
| // Initial channel was requested (multiple renderers may be launched, e.g. |
| // spare). |
| EXPECT_FALSE( |
| histogram_tester |
| .GetAllSamples("GPU.EstablishGpuChannel.Browser.InitAsyncLatency") |
| .empty()); |
| histogram_tester.ExpectTotalCount( |
| "GPU.EstablishGpuChannel.Browser.RegularAsyncLatency", 0); |
| EXPECT_FALSE( |
| histogram_tester |
| .GetAllSamples( |
| "GPU.EstablishGpuChannel.InitialChannelBrowserToRendererLatency") |
| .empty()); |
| EXPECT_FALSE( |
| histogram_tester |
| .GetAllSamples("GPU.EstablishGpuChannel.InitialChannelLatency") |
| .empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelBrowserTest, |
| SpareRendererUsesEarlyChannel) { |
| // Ensure a spare renderer is created. |
| auto* browser_context = shell()->web_contents()->GetBrowserContext(); |
| SpareRenderProcessHostManagerImpl::Get().WarmupSpare(browser_context); |
| |
| // Verify that the spare renderer has been created. |
| EXPECT_TRUE(SpareRenderProcessHostManagerImpl::Get().HasSpareRenderer()); |
| |
| // Navigate to a page. This should adopt the spare renderer. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| // Verify that the renderer is able to use the early channel to submit frames. |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| frame_observer.SetWaitForNextFrame(); |
| |
| // Verify that rendering works. |
| EXPECT_EQ(true, EvalJs(shell(), GetForceCompositeScript())); |
| |
| // Wait for Viz to composite the frames. |
| frame_observer.WaitForNextFrameSubmission(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelBrowserTest, |
| GpuChannelCancellationScenarios) { |
| // Navigate to a page to ensure renderer process is started. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| // Hide WebContents to prevent renderer from using gpu channel right away. |
| shell()->web_contents()->WasHidden(); |
| |
| auto* rphi = static_cast<RenderProcessHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| auto* gpu_client = rphi->GetGpuClient(); |
| |
| // Helper to ignore arguments of EstablishGpuChannel callback. |
| auto ignore_args = [](base::OnceClosure closure) { |
| return base::BindOnce( |
| [](base::OnceClosure closure, int32_t client_id, |
| mojo::ScopedMessagePipeHandle channel_handle, |
| const gpu::GPUInfo& gpu_info, |
| const gpu::GpuFeatureInfo& gpu_feature_info, |
| const gpu::SharedImageCapabilities& shared_image_capabilities) { |
| std::move(closure).Run(); |
| }, |
| std::move(closure)); |
| }; |
| |
| // Scenario 1: InitializeGpuChannelForNewRenderer cancels |
| // InitializeGpuChannelForNewRenderer |
| { |
| mojo::MessagePipe pipe1; |
| gpu_client->InitializeGpuChannelForNewRenderer(std::move(pipe1.handle0)); |
| |
| mojo::MessagePipe pipe2; |
| gpu_client->InitializeGpuChannelForNewRenderer(std::move(pipe2.handle0)); |
| |
| // The first request should be cancelled. We can verify by checking if we |
| // can still establish a channel. |
| base::test::TestFuture<void> establish_future; |
| gpu_client->EstablishGpuChannel( |
| ignore_args(establish_future.GetCallback())); |
| EXPECT_TRUE(establish_future.Wait()); |
| } |
| |
| // Scenario 2: Establish cancels InitializeGpuChannelForNewRenderer |
| { |
| mojo::MessagePipe pipe; |
| gpu_client->InitializeGpuChannelForNewRenderer(std::move(pipe.handle0)); |
| |
| base::test::TestFuture<void> establish_future; |
| gpu_client->EstablishGpuChannel( |
| ignore_args(establish_future.GetCallback())); |
| EXPECT_TRUE(establish_future.Wait()); |
| } |
| |
| // Scenario 3: InitializeGpuChannelForNewRenderer cancels Establish |
| { |
| base::test::TestFuture<bool> establish_future; |
| gpu_client->EstablishGpuChannel(base::BindOnce( |
| [](base::OnceCallback<void(bool)> callback, int32_t client_id, |
| mojo::ScopedMessagePipeHandle channel_handle, |
| const gpu::GPUInfo& gpu_info, |
| const gpu::GpuFeatureInfo& gpu_feature_info, |
| const gpu::SharedImageCapabilities& shared_image_capabilities) { |
| std::move(callback).Run(channel_handle.is_valid()); |
| }, |
| establish_future.GetCallback())); |
| |
| EXPECT_FALSE(establish_future.IsReady()); |
| |
| mojo::MessagePipe pipe; |
| gpu_client->InitializeGpuChannelForNewRenderer(std::move(pipe.handle0)); |
| |
| // The pending EstablishGpuChannel request's callback should be called with |
| // failure. |
| EXPECT_FALSE(establish_future.Get()); |
| |
| // Wait for the InitializeGpuChannelForNewRenderer request to complete. |
| base::test::TestFuture<bool> success_future; |
| gpu_client->SetEstablishGpuChannelCallbackForTesting( |
| success_future.GetCallback()); |
| EXPECT_TRUE(success_future.Get()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelBrowserTest, |
| EarlyFrameSinkUsed_InitiallyHidden) { |
| // Hide the WebContents before navigation. |
| shell()->web_contents()->WasHidden(); |
| |
| // Navigate to a page. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| frame_observer.SetWaitForNextFrame(); |
| |
| auto* rwhi = static_cast<RenderWidgetHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| |
| // On non-ChromeOS platforms, the frame sink pipes should be created but |
| // deferred because we are hidden. |
| #if BUILDFLAG(IS_CHROMEOS) |
| EXPECT_FALSE(rwhi->has_initial_frame_sink_for_testing()); |
| #else |
| // Note that there might be some racy Show() call before this, which might |
| // trigger the sending of the initial frame sink pipe earlier. |
| EXPECT_EQ(rwhi->is_hidden_for_testing(), |
| rwhi->has_initial_frame_sink_for_testing()); |
| #endif |
| |
| // Show the WebContents and focus it. |
| shell()->web_contents()->WasShown(); |
| shell()->web_contents()->Focus(); |
| |
| // The pipes should have been used and cleared! |
| EXPECT_FALSE(rwhi->has_initial_frame_sink_for_testing()); |
| |
| // Ping the renderer to ensure WasShown() and UpdateVisualProperties() Mojo |
| // messages are fully processed, and the early frame sink is bound. |
| EXPECT_EQ(true, EvalJs(shell(), "true")); |
| |
| // Force a repaint on a new surface. This guarantees a new frame is drawn |
| // and submitted to Viz even if there is no other page damage. |
| EXPECT_TRUE(rwhi->RequestRepaintOnNewSurface()); |
| |
| // Wait for a frame to be submitted. This ensures the renderer has processed |
| // the WasShown() call and requested the frame sink. |
| frame_observer.WaitForNextFrameSubmission(); |
| } |
| |
| class InitialGpuChannelDisabledBrowserTest : public ContentBrowserTest { |
| public: |
| InitialGpuChannelDisabledBrowserTest() { |
| feature_list_.InitAndDisableFeature(features::kSendGPUChannelEarly); |
| } |
| |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelDisabledBrowserTest, |
| FallsBackToStandardInitialization) { |
| // Navigate to a page. |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| frame_observer.SetWaitForNextFrame(); |
| |
| // Verify rendering still works. |
| EXPECT_EQ(true, EvalJs(shell(), GetForceCompositeScript())); |
| |
| // Wait for Viz to composite the frames. |
| frame_observer.WaitForNextFrameSubmission(); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| |
| namespace { |
| class InitialWebUIOverrideContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| explicit InitialWebUIOverrideContentBrowserClient( |
| const GURL& initial_webui_url) |
| : initial_webui_url_(initial_webui_url) {} |
| |
| bool IsInitialWebUIURL(const GURL& url) override { |
| return initial_webui_url_ == url; |
| } |
| bool IsTopChromeWebUIURL(const GURL& url) override { |
| return initial_webui_url_ == url; |
| } |
| |
| private: |
| GURL initial_webui_url_; |
| }; |
| |
| class WebUITestWebUIControllerFactory : public WebUIControllerFactory { |
| public: |
| std::unique_ptr<WebUIController> CreateWebUIControllerForURL( |
| WebUI* web_ui, |
| const GURL& url) override { |
| return HasWebUIScheme(url) ? std::make_unique<WebUIController>(web_ui) |
| : nullptr; |
| } |
| WebUI::TypeID GetWebUIType(BrowserContext* browser_context, |
| const GURL& url) override { |
| return HasWebUIScheme(url) ? reinterpret_cast<WebUI::TypeID>(1) : nullptr; |
| } |
| bool UseWebUIForURL(BrowserContext* browser_context, |
| const GURL& url) override { |
| return HasWebUIScheme(url); |
| } |
| }; |
| |
| } // namespace |
| |
| class TestWebUIV2EnabledConfig : public DefaultWebUIConfig<WebUIController> { |
| public: |
| TestWebUIV2EnabledConfig(std::string_view scheme, std::string_view host) |
| : DefaultWebUIConfig(scheme, host) {} |
| |
| bool SupportsInProcessResourceLoadingV2() const override { return true; } |
| }; |
| |
| class InitialGpuChannelForTopChromeWebUIOnlyBrowserTest |
| : public ContentBrowserTest { |
| public: |
| InitialGpuChannelForTopChromeWebUIOnlyBrowserTest() { |
| WebUIControllerFactory::RegisterFactory(&factory_); |
| feature_list_.InitWithFeaturesAndParameters( |
| {{features::kSendGPUChannelEarly, |
| {{"for_topchrome_webui_only", "true"}}}, |
| {features::kWebUIInProcessResourceLoadingV2, {}}}, |
| {/* disabled_features */}); |
| } |
| |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| private: |
| WebUITestWebUIControllerFactory factory_; |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelForTopChromeWebUIOnlyBrowserTest, |
| FallsBackToStandardInitializationForRegularPages) { |
| shell()->web_contents()->WasHidden(); |
| |
| // Navigate to a regular page (not a Top Chrome WebUI). |
| EXPECT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); |
| |
| auto* rwhi = static_cast<RenderWidgetHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| |
| // Since this is a regular page, early GPU channel and frame sink |
| // optimizations are disabled by the feature parameter constraint. |
| EXPECT_FALSE(rwhi->has_initial_frame_sink_for_testing()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(InitialGpuChannelForTopChromeWebUIOnlyBrowserTest, |
| UsesEarlyChannelForInitialWebUI) { |
| GURL url("chrome://foo"); |
| |
| auto& config_map = WebUIConfigMap::GetInstance(); |
| config_map.AddWebUIConfig( |
| std::make_unique<TestWebUIV2EnabledConfig>("chrome", "foo")); |
| base::ScopedClosureRunner cleanup_configs(base::BindLambdaForTesting([&]() { |
| WebUIConfigMap::GetInstance().RemoveConfig(GURL("chrome://foo")); |
| })); |
| |
| InitialWebUIOverrideContentBrowserClient content_browser_client(url); |
| |
| // Create a new WebContents to ensure it is the very first navigation, |
| // which is required for initial WebUI navigations. Ensure it starts hidden |
| // so we can inspect the deferred initial frame sink creation. |
| WebContents::CreateParams new_contents_params( |
| shell()->web_contents()->GetBrowserContext(), |
| shell()->web_contents()->GetSiteInstance()); |
| new_contents_params.site_instance = SiteInstance::CreateForURL( |
| shell()->web_contents()->GetBrowserContext(), url); |
| std::unique_ptr<WebContents> new_web_contents( |
| WebContents::Create(new_contents_params)); |
| new_web_contents->WasHidden(); |
| |
| WebUIDataSource* source = WebUIDataSource::CreateAndAdd( |
| new_web_contents->GetBrowserContext(), "foo"); |
| source->SetResourcePathToResponse("", "<!doctype html><body>bar</body>"); |
| |
| TestNavigationObserver navigation_observer(url); |
| navigation_observer.WatchExistingWebContents(); |
| new_web_contents->GetController().LoadURLWithParams( |
| NavigationController::LoadURLParams(url)); |
| navigation_observer.Wait(); |
| |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(navigation_observer.last_navigation_url(), url); |
| |
| auto* rwhi = static_cast<RenderWidgetHostImpl*>( |
| new_web_contents->GetPrimaryMainFrame()->GetRenderWidgetHost()); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| EXPECT_FALSE(rwhi->has_initial_frame_sink_for_testing()); |
| #else |
| EXPECT_EQ(rwhi->is_hidden_for_testing(), |
| rwhi->has_initial_frame_sink_for_testing()); |
| #endif |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| } // namespace content |