blob: 8b062e28c895bc3488737760518ee7e8e7894cb9 [file]
// 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