blob: 1b4e52c6a570635c3e22630145aa651d04ea191e [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/performance_manager/v8_memory/v8_context_tracker.h"
#include <memory>
#include "base/location.h"
#include "base/strings/stringprintf.h"
#include "base/command_line.h"
#include "components/performance_manager/execution_context/execution_context_registry_impl.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/performance_manager.h"
#include "components/performance_manager/test_support/performance_manager_browsertest_harness.h"
#include "components/performance_manager/test_support/run_in_graph.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "content/public/test/test_utils.h"
namespace performance_manager {
namespace v8_memory {
struct ContextCounts {
size_t v8_context_count = 0;
size_t execution_context_count = 0;
size_t detached_v8_context_count = 0;
size_t destroyed_execution_context_count = 0;
};
class V8ContextTrackerTest : public PerformanceManagerBrowserTestHarness {
public:
using Super = PerformanceManagerBrowserTestHarness;
V8ContextTrackerTest() = default;
~V8ContextTrackerTest() override = default;
void SetUp() override {
GetGraphFeatures().EnableV8ContextTracker();
Super::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Force site-per-process so the number of internal utility v8 contexts is
// stable.
content::IsolateAllSitesForTesting(command_line);
Super::SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
RunInGraph([&](Graph* graph) {
auto* v8ct = V8ContextTracker::GetFromGraph(graph);
ASSERT_TRUE(v8ct);
// The browser could start with execution contexts and/or v8 contexts (for
// example if it creates a spare renderer loading about:blank, or
// something preloads a utility context).
current_counts_.v8_context_count = v8ct->GetV8ContextCountForTesting();
current_counts_.execution_context_count =
v8ct->GetExecutionContextCountForTesting();
// There should not be any detached or destroyed contexts on start.
EXPECT_EQ(v8ct->GetDetachedV8ContextCountForTesting(), 0u);
EXPECT_EQ(v8ct->GetDestroyedExecutionContextCountForTesting(), 0u);
});
Super::SetUpOnMainThread();
}
void ExpectCountIncrease(
ContextCounts count_change,
const base::Location& location = base::Location::Current()) {
RunInGraph([&](Graph* graph) {
SCOPED_TRACE(location.ToString());
auto* v8ct = V8ContextTracker::GetFromGraph(graph);
ASSERT_TRUE(v8ct);
// There may be extra V8 contexts created, such as for lazily-created
// utility contexts.
EXPECT_GE(
v8ct->GetV8ContextCountForTesting(),
current_counts_.v8_context_count + count_change.v8_context_count)
<< "expected increase " << count_change.v8_context_count;
current_counts_.v8_context_count = v8ct->GetV8ContextCountForTesting();
EXPECT_EQ(v8ct->GetExecutionContextCountForTesting(),
current_counts_.execution_context_count +
count_change.execution_context_count)
<< "expected increase " << count_change.execution_context_count;
current_counts_.execution_context_count =
v8ct->GetExecutionContextCountForTesting();
EXPECT_EQ(v8ct->GetDetachedV8ContextCountForTesting(),
current_counts_.detached_v8_context_count +
count_change.detached_v8_context_count)
<< "expected increase " << count_change.detached_v8_context_count;
current_counts_.detached_v8_context_count =
v8ct->GetDetachedV8ContextCountForTesting();
EXPECT_EQ(v8ct->GetDestroyedExecutionContextCountForTesting(),
current_counts_.destroyed_execution_context_count +
count_change.destroyed_execution_context_count)
<< "expected increase "
<< count_change.destroyed_execution_context_count;
;
current_counts_.destroyed_execution_context_count =
v8ct->GetDestroyedExecutionContextCountForTesting();
});
}
private:
ContextCounts current_counts_;
};
// TODO(crbug.com/40931300): Re-enable on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_AboutBlank DISABLED_AboutBlank
#else
#define MAYBE_AboutBlank AboutBlank
#endif
IN_PROC_BROWSER_TEST_F(V8ContextTrackerTest, MAYBE_AboutBlank) {
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
ExpectCountIncrease({.v8_context_count = 1, .execution_context_count = 1});
}
IN_PROC_BROWSER_TEST_F(V8ContextTrackerTest, SameOriginIframeAttributionData) {
GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_a.html"));
auto* contents = shell()->web_contents();
ASSERT_TRUE(
NavigateAndWaitForConsoleMessage(contents, urla, "a.html loaded"));
// Get pointers to the RFHs for each frame.
content::RenderFrameHost* main_rfh = contents->GetPrimaryMainFrame();
content::RenderFrameHost* child_rfh = ChildFrameAt(main_rfh, 0);
ASSERT_TRUE(child_rfh);
auto frame_node =
PerformanceManager::GetFrameNodeForRenderFrameHost(child_rfh);
RunInGraph([&frame_node](Graph* graph) {
ASSERT_TRUE(frame_node);
auto* v8_context_tracker = V8ContextTracker::GetFromGraph(graph);
ASSERT_TRUE(v8_context_tracker);
auto* ec_state = v8_context_tracker->GetExecutionContextState(
frame_node->GetFrameToken());
ASSERT_TRUE(ec_state);
ASSERT_TRUE(ec_state->iframe_attribution_data);
});
}
// TODO(crbug.com/40931300): Re-enable on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_CrossOriginIframeAttributionData \
DISABLED_CrossOriginIframeAttributionData
#else
#define MAYBE_CrossOriginIframeAttributionData CrossOriginIframeAttributionData
#endif // BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(V8ContextTrackerTest,
MAYBE_CrossOriginIframeAttributionData) {
GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_b.html"));
auto* contents = shell()->web_contents();
ASSERT_TRUE(
NavigateAndWaitForConsoleMessage(contents, urla, "b.html loaded"));
// Get pointers to the RFHs for each frame.
content::RenderFrameHost* main_rfh = contents->GetPrimaryMainFrame();
content::RenderFrameHost* child_rfh = ChildFrameAt(main_rfh, 0);
ASSERT_TRUE(child_rfh);
auto frame_node =
PerformanceManager::GetFrameNodeForRenderFrameHost(child_rfh);
RunInGraph([&frame_node](Graph* graph) {
ASSERT_TRUE(frame_node);
auto* v8_context_tracker = V8ContextTracker::GetFromGraph(graph);
ASSERT_TRUE(v8_context_tracker);
auto* ec_state = v8_context_tracker->GetExecutionContextState(
frame_node->GetFrameToken());
ASSERT_TRUE(ec_state);
ASSERT_TRUE(ec_state->iframe_attribution_data)
<< "url " << frame_node->GetURL() << ", current "
<< frame_node->IsCurrent() << ", state "
<< frame_node->GetLifecycleState();
});
}
// TODO(crbug.com/40931300): Re-enable on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_SameSiteNavigation DISABLED_SameSiteNavigation
#else
#define MAYBE_SameSiteNavigation SameSiteNavigation
#endif // BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(V8ContextTrackerTest, MAYBE_SameSiteNavigation) {
auto* contents = shell()->web_contents();
GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_b.html"));
ASSERT_TRUE(
NavigateAndWaitForConsoleMessage(contents, urla, "b.html loaded"));
ExpectCountIncrease({.v8_context_count = 2, .execution_context_count = 2});
// Get pointers to the RFHs for each frame.
content::RenderFrameHost* rfha = contents->GetPrimaryMainFrame();
content::RenderFrameHost* rfhb = ChildFrameAt(rfha, 0);
bool rfh_should_change =
rfhb->ShouldChangeRenderFrameHostOnSameSiteNavigation();
// Execute a same site navigation in the child frame. This causes a
// v8 context to be detached, and new context attached to the execution
// context.
GURL urlb(embedded_test_server()->GetURL("b.com", "/b.html?foo=bar"));
ASSERT_TRUE(ExecJs(
rfhb, base::StringPrintf("location.href = \"%s\"", urlb.spec().c_str())));
WaitForLoad(contents);
if (rfh_should_change) {
// When RenderDocument is enabled, a new RenderFrameHost will be created for
// the navigation to `urlb`. Both a new V8 context and ExecutionContext are
// created, and the old ExecutionContext is destroyed..
ExpectCountIncrease({.v8_context_count = 1,
.execution_context_count = 1,
.detached_v8_context_count = 1,
.destroyed_execution_context_count = 1});
} else {
// When RenderDocument is disabled, the same RenderFrameHost will be reused
// for the navigation to `urlb`. So only a new V8 context will be created,
// not a new ExecutionContext.
ExpectCountIncrease(
{.v8_context_count = 1, .detached_v8_context_count = 1});
}
}
IN_PROC_BROWSER_TEST_F(V8ContextTrackerTest, DetachedContext) {
auto* contents = shell()->web_contents();
GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_a.html"));
ASSERT_TRUE(
NavigateAndWaitForConsoleMessage(contents, urla, "a.html loaded"));
ExpectCountIncrease({.v8_context_count = 2, .execution_context_count = 2});
// Get pointers to the RFHs for each frame.
content::RenderFrameHost* rfha = contents->GetPrimaryMainFrame();
// Keep a pointer to the window associated with the child iframe, but
// unload it.
ASSERT_TRUE(ExecJs(rfha,
"let iframe = document.getElementsByTagName('iframe')[0]; "
"document.body.leakyRef = iframe.contentWindow.window; "
"iframe.parentNode.removeChild(iframe); "
"console.log('detached and leaked iframe');"));
ExpectCountIncrease({
.detached_v8_context_count = 1,
.destroyed_execution_context_count = 1,
});
}
} // namespace v8_memory
} // namespace performance_manager