Commit 93a5b659 authored by Brian Geffon's avatar Brian Geffon Committed by Commit Bot

CrOS: userspace swap: introduce the userspace swap policy.

This CL introduces the userspace swap policy as described in
go/userspace-swap. The policy is only concerned with the "when"
of userspace swap and not the actual userspace swap implementation
itself. The swapping part will be handled in a later CL which will
contain the "mechanism" part of userspace swapping.

This CL uses information about renderers to determine if they are
eligible to be swapped and if so it will trigger the userspace
swap process to the mechanism.

BUG=chromium:1067833

Change-Id: I9a1f635c96401701f4c1cad44ba403582f2af828
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2295926
Commit-Queue: Brian Geffon <bgeffon@chromium.org>
Reviewed-by: default avatarChris Hamilton <chrisha@chromium.org>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarMatthew Denton <mpdenton@chromium.org>
Cr-Commit-Position: refs/heads/master@{#791615}
parent 1549acb5
......@@ -3916,6 +3916,8 @@ static_library("browser") {
"notifications/web_page_notifier_controller.h",
"performance_manager/mechanisms/working_set_trimmer_chromeos.cc",
"performance_manager/mechanisms/working_set_trimmer_chromeos.h",
"performance_manager/policies/userspace_swap_policy_chromeos.cc",
"performance_manager/policies/userspace_swap_policy_chromeos.h",
"performance_manager/policies/working_set_trimmer_policy_chromeos.cc",
"performance_manager/policies/working_set_trimmer_policy_chromeos.h",
"platform_util_chromeos.cc",
......
......@@ -35,10 +35,13 @@
#if defined(OS_CHROMEOS)
#include "base/allocator/buildflags.h"
#include "chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.h"
#if BUILDFLAG(USE_TCMALLOC)
#include "chrome/browser/performance_manager/policies/dynamic_tcmalloc_policy_chromeos.h"
#include "chrome/common/performance_manager/mojom/tcmalloc.mojom.h"
#endif // BUILDFLAG(USE_TCMALLOC)
#endif // defined(OS_CHROMEOS)
#if !defined(OS_ANDROID)
......@@ -91,6 +94,12 @@ void ChromeBrowserMainExtraPartsPerformanceManager::CreatePoliciesAndDecorators(
}
#if defined(OS_CHROMEOS)
if (performance_manager::policies::UserspaceSwapPolicy::
UserspaceSwapSupportedAndEnabled()) {
graph->PassToGraph(
std::make_unique<performance_manager::policies::UserspaceSwapPolicy>());
}
#if BUILDFLAG(USE_TCMALLOC)
if (base::FeatureList::IsEnabled(
performance_manager::features::kDynamicTcmallocTuning)) {
......
// Copyright 2020 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.
#include "chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.h"
#include "base/time/time.h"
#include "chromeos/memory/userspace_swap/swap_storage.h"
#include "chromeos/memory/userspace_swap/userspace_swap.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/node_attached_data.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/graph/process_node.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
namespace performance_manager {
namespace policies {
namespace {
using chromeos::memory::userspace_swap::SwapFile;
using chromeos::memory::userspace_swap::UserspaceSwapConfig;
class UserspaceSwapPolicyData
: public ExternalNodeAttachedDataImpl<UserspaceSwapPolicyData> {
public:
explicit UserspaceSwapPolicyData(const ProcessNode* node) {}
~UserspaceSwapPolicyData() override = default;
static UserspaceSwapPolicyData* EnsureForProcess(
const ProcessNode* process_node) {
return UserspaceSwapPolicyData::GetOrCreate(process_node);
}
bool initialization_attempted_ = false;
bool process_initialized_ = false;
base::TimeTicks last_swap_;
};
#ifndef NDEBUG
constexpr base::TimeDelta kMetricsInterval = base::TimeDelta::FromSeconds(30);
#endif
constexpr base::TimeDelta kSwapDeviceAvailableSpaceCheckInterval =
base::TimeDelta::FromSeconds(30);
} // namespace
UserspaceSwapPolicy::UserspaceSwapPolicy(const UserspaceSwapConfig& config)
: config_(config) {
DCHECK(config_.enabled);
#ifndef NDEBUG
if (!metrics_timer_->IsRunning()) {
metrics_timer_->Start(
FROM_HERE, kMetricsInterval,
base::BindRepeating(&UserspaceSwapPolicy::PrintAllSwapMetrics,
weak_factory_.GetWeakPtr()));
}
#endif // ifndef NDEBUG
}
UserspaceSwapPolicy::UserspaceSwapPolicy()
: UserspaceSwapPolicy(UserspaceSwapConfig::Get()) {}
UserspaceSwapPolicy::~UserspaceSwapPolicy() = default;
void UserspaceSwapPolicy::OnPassedToGraph(Graph* graph) {
DCHECK_EQ(graph_, nullptr);
graph_ = graph;
graph->AddProcessNodeObserver(this);
// Only create a memory pressure listener if the feature to swap on moderate
// pressure is enabled.
if (config_.swap_on_moderate_pressure) {
memory_pressure_listener_.emplace(
FROM_HERE, base::BindRepeating(&UserspaceSwapPolicy::OnMemoryPressure,
weak_factory_.GetWeakPtr()));
}
}
void UserspaceSwapPolicy::OnTakenFromGraph(Graph* graph) {
DCHECK_EQ(graph_, graph);
memory_pressure_listener_.reset();
graph->RemoveProcessNodeObserver(this);
graph_ = nullptr;
}
void UserspaceSwapPolicy::OnAllFramesInProcessFrozen(
const ProcessNode* process_node) {
if (config_.swap_on_freeze) {
// We don't provide a page node because the visibility requirements don't
// matter on freeze.
if (IsEligibleToSwap(process_node, nullptr)) {
VLOG(1) << "pid: " << process_node->GetProcessId() << " swap on freeze";
UserspaceSwapPolicyData::EnsureForProcess(process_node)->last_swap_ =
base::TimeTicks::Now();
SwapProcessNode(process_node);
}
}
}
void UserspaceSwapPolicy::OnProcessNodeAdded(const ProcessNode* process_node) {
// If data was still associated with this node make sure it's blown away and
// any existing file descriptors are closed.
if (UserspaceSwapPolicyData::Destroy(process_node)) {
DLOG(FATAL)
<< "ProcessNode had a UserspaceSwapPolicyData attached when added.";
}
}
bool UserspaceSwapPolicy::InitializeProcessNode(
const ProcessNode* process_node) {
// TODO(bgeffon): This will be dispatched to the userspace swap mechanism when
// it's committed.
return false;
}
void UserspaceSwapPolicy::OnProcessLifetimeChange(
const ProcessNode* process_node) {
if (!process_node->GetProcess().IsValid()) {
return;
}
UserspaceSwapPolicyData* data =
UserspaceSwapPolicyData::EnsureForProcess(process_node);
if (!data->initialization_attempted_) {
data->initialization_attempted_ = true;
// If this fails we don't attempt swap ever.
data->process_initialized_ = InitializeProcessNode(process_node);
}
}
base::TimeTicks UserspaceSwapPolicy::GetLastSwapTime(
const ProcessNode* process_node) {
return UserspaceSwapPolicyData::EnsureForProcess(process_node)->last_swap_;
}
void UserspaceSwapPolicy::OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel level) {
if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE)
return;
auto now_ticks = base::TimeTicks::Now();
// Try not to walk the graph too frequently because we can receive moderate
// memory pressure notifications every 10s.
if (now_ticks - last_graph_walk_ < config_.graph_walk_frequency) {
return;
}
last_graph_walk_ = now_ticks;
SwapNodesOnGraph();
}
void UserspaceSwapPolicy::SwapNodesOnGraph() {
for (const PageNode* page_node : graph_->GetAllPageNodes()) {
// Check that we have a main frame.
const FrameNode* main_frame_node = page_node->GetMainFrameNode();
if (!main_frame_node)
continue;
const ProcessNode* process_node = main_frame_node->GetProcessNode();
if (IsEligibleToSwap(process_node, page_node)) {
VLOG(1) << "pid: " << process_node->GetProcessId()
<< " trigger swap for frame " << main_frame_node->GetURL();
UserspaceSwapPolicyData::EnsureForProcess(process_node)->last_swap_ =
base::TimeTicks::Now();
SwapProcessNode(process_node);
}
}
}
void UserspaceSwapPolicy::PrintAllSwapMetrics() {
uint64_t total_reclaimed = 0;
uint64_t total_on_disk = 0;
uint64_t total_renderers = 0;
for (const PageNode* page_node : graph_->GetAllPageNodes()) {
const FrameNode* main_frame_node = page_node->GetMainFrameNode();
if (!main_frame_node)
continue;
const ProcessNode* process_node = main_frame_node->GetProcessNode();
auto now_ticks = base::TimeTicks::Now();
if (process_node && process_node->GetProcess().IsValid()) {
bool is_visible = page_node->IsVisible();
auto last_visibility_change =
page_node->GetTimeSinceLastVisibilityChange();
auto url = main_frame_node->GetURL();
uint64_t memory_reclaimed = GetProcessNodeReclaimedBytes(process_node);
uint64_t disk_space_used = GetProcessNodeSwapFileUsageBytes(process_node);
total_on_disk += disk_space_used;
total_reclaimed += memory_reclaimed;
total_renderers++;
VLOG(1) << "Frame " << url << " visibile: " << is_visible
<< " last_chg: " << last_visibility_change
<< " last_swap: " << (now_ticks - GetLastSwapTime(process_node))
<< " reclaimed: " << (memory_reclaimed >> 10) << "Kb"
<< " on disk: " << (disk_space_used >> 10) << "Kb";
}
}
VLOG(1) << "Swap Summary, Renderers: " << total_renderers
<< " reclaimed: " << (total_reclaimed >> 10)
<< "Kb, total on disk: " << (total_on_disk >> 10) << "Kb"
<< " Backing Store free space: "
<< (GetSwapDeviceFreeSpaceBytes() >> 10) << "Kb";
}
void UserspaceSwapPolicy::SwapProcessNode(const ProcessNode* process_node) {
// TODO(bgeffon): This will trigger the swap in the userspace swap mechanism
// once it has landed.
}
uint64_t UserspaceSwapPolicy::GetProcessNodeSwapFileUsageBytes(
const ProcessNode* process_node) {
// TODO(bgeffon): Once the mechanism is landed we will query it for the swap
// file usage for a specific renderer.
return 0;
}
uint64_t UserspaceSwapPolicy::GetProcessNodeReclaimedBytes(
const ProcessNode* process_node) {
// TODO(bgeffon): Once the mechanism is landed we will query it for the swap
// file usage for a specific renderer.
return 0;
}
uint64_t UserspaceSwapPolicy::GetTotalSwapFileUsageBytes() {
// TODO(bgeffon): Once the mechanism is landed we will query it for the swap
// file usage for a specific renderer.
return 0;
}
uint64_t UserspaceSwapPolicy::GetSwapDeviceFreeSpaceBytes() {
return SwapFile::GetBackingStoreFreeSpaceKB() << 10; // Convert to bytes.
}
bool UserspaceSwapPolicy::IsPageNodeLoading(const PageNode* page_node) {
return page_node->IsLoading();
}
bool UserspaceSwapPolicy::IsPageNodeAudible(const PageNode* page_node) {
return page_node->IsAudible();
}
bool UserspaceSwapPolicy::IsPageNodeVisible(const PageNode* page_node) {
return page_node->IsVisible();
}
base::TimeDelta UserspaceSwapPolicy::GetTimeSinceLastVisibilityChange(
const PageNode* page_node) {
return page_node->GetTimeSinceLastVisibilityChange();
}
bool UserspaceSwapPolicy::IsEligibleToSwap(const ProcessNode* process_node,
const PageNode* page_node) {
if (!process_node || !process_node->GetProcess().IsValid()) {
LOG(ERROR) << "Process node not valid";
return false;
}
auto* data = UserspaceSwapPolicyData::EnsureForProcess(process_node);
if (!data->process_initialized_) {
LOG(ERROR) << "Process not initialized";
return false;
}
auto now_ticks = base::TimeTicks::Now();
// Don't swap a renderer too frequently.
auto time_since_last_swap = now_ticks - GetLastSwapTime(process_node);
if (time_since_last_swap < config_.process_swap_frequency) {
return false;
}
// If the caller provided a PageNode we will validate the visibility state of
// it.
if (page_node) {
// If we're loading, audible, or visible we will not swap.
if (IsPageNodeLoading(page_node) || IsPageNodeVisible(page_node) ||
IsPageNodeAudible(page_node)) {
return false;
}
// Next the page node must have been invisible for longer than the
// configured time.
if (GetTimeSinceLastVisibilityChange(page_node) <
config_.invisible_time_before_swap) {
return false;
}
}
// To avoid hammering the system with fstat(2) system calls we will cache the
// available disk space for 30 seconds. But we only check if it's been
// configured to enforce a swap device minimum.
if (config_.minimum_swap_disk_space_available > 0) {
auto time_since_last_device_space_check =
now_ticks - last_device_space_check_;
if (time_since_last_device_space_check >
kSwapDeviceAvailableSpaceCheckInterval) {
last_device_space_check_ = now_ticks;
backing_store_available_bytes_ = GetSwapDeviceFreeSpaceBytes();
}
// Check if we can't swap because the device is running low on space.
if (backing_store_available_bytes_ <
config_.minimum_swap_disk_space_available) {
return false;
}
}
// Make sure we're not exceeding the total swap file usage across all
// renderers.
if (config_.maximum_swap_disk_space_bytes > 0) {
if (GetTotalSwapFileUsageBytes() >= config_.maximum_swap_disk_space_bytes) {
return false;
}
}
// And make sure we're not exceeding the per-renderer swap file limit.
if (config_.renderer_maximum_disk_swap_file_size_bytes > 0) {
if (GetProcessNodeSwapFileUsageBytes(process_node) >=
config_.renderer_maximum_disk_swap_file_size_bytes) {
return false;
}
}
return true;
}
// Static
bool UserspaceSwapPolicy::UserspaceSwapSupportedAndEnabled() {
static bool enabled = UserspaceSwapConfig::Get().enabled;
static bool supported =
chromeos::memory::userspace_swap::KernelSupportsUserspaceSwap();
return enabled && supported;
}
} // namespace policies
} // namespace performance_manager
// Copyright 2020 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.
#ifndef CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_USERSPACE_SWAP_POLICY_CHROMEOS_H_
#define CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_USERSPACE_SWAP_POLICY_CHROMEOS_H_
#include "base/macros.h"
#include "base/memory/memory_pressure_listener.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit_observer.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/graph/process_node.h"
namespace chromeos {
namespace memory {
namespace userspace_swap {
struct UserspaceSwapConfig;
} // namespace userspace_swap
} // namespace memory
} // namespace chromeos
namespace performance_manager {
namespace policies {
// UserspaceSwapPolicy is a policy which will trigger a renderer to swap itself
// via userspace.
class UserspaceSwapPolicy : public GraphOwned,
public ProcessNode::ObserverDefaultImpl {
public:
UserspaceSwapPolicy();
~UserspaceSwapPolicy() override;
// GraphOwned implementation:
void OnPassedToGraph(Graph* graph) override;
void OnTakenFromGraph(Graph* graph) override;
// ProcessNodeObserver implementation:
void OnAllFramesInProcessFrozen(const ProcessNode* process_node) override;
void OnProcessNodeAdded(const ProcessNode* process_node) override;
void OnProcessLifetimeChange(const ProcessNode* process_node) override;
// MemoryPressureListener impl:
void OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel level);
// Returns true if running on a platform that supports the kernel features
// necessary for userspace swapping, most important would be userfaultfd(2).
static bool UserspaceSwapSupportedAndEnabled();
protected:
explicit UserspaceSwapPolicy(
const chromeos::memory::userspace_swap::UserspaceSwapConfig& config);
// The following methods are virtual for testing.
virtual void SwapNodesOnGraph();
virtual bool InitializeProcessNode(const ProcessNode* process_node);
virtual uint64_t GetTotalSwapFileUsageBytes();
virtual uint64_t GetSwapDeviceFreeSpaceBytes();
virtual void SwapProcessNode(const ProcessNode* process_node);
virtual uint64_t GetProcessNodeSwapFileUsageBytes(
const ProcessNode* process_node);
virtual uint64_t GetProcessNodeReclaimedBytes(
const ProcessNode* process_node);
virtual bool IsPageNodeVisible(const PageNode* page_node);
virtual bool IsPageNodeAudible(const PageNode* page_node);
virtual bool IsPageNodeLoading(const PageNode* page_node);
virtual base::TimeDelta GetTimeSinceLastVisibilityChange(
const PageNode* page_node);
// IsEligibleToSwap will return true if the |page_node| belonging to the
// |process_node| meets the configured criteria to be swap eligible. The
// |page_node| is optional, if it's provided the visibility, audible, and
// loading states will also be used to determine eligibility. The situation
// where the |page_node| is omitted is when the process is frozen.
virtual bool IsEligibleToSwap(const ProcessNode* process_node,
const PageNode* page_node);
// Returns the time in which this process was last swapped, |process_node| is
// the node in which you want to update.
virtual base::TimeTicks GetLastSwapTime(const ProcessNode* process_node);
// We cache the config object since it cannot change, this makes the code
// easier to read and testing also becomes easier.
const chromeos::memory::userspace_swap::UserspaceSwapConfig& config_;
// Keeps track of the last time we walked the graph looking for processes to
// swap, the frequency we walk the graph is configurable.
base::TimeTicks last_graph_walk_;
// We periodically check the amount of free space on the device that backs our
// swap files to make sure we're not letting it get below the configured
// limit, we don't want to completely exhaust free space on a device.
base::TimeTicks last_device_space_check_;
uint64_t backing_store_available_bytes_ = 0;
Graph* graph_ = nullptr;
base::Optional<base::MemoryPressureListener> memory_pressure_listener_;
private:
// A helper method which sets the last trim time to the specified time.
void SetLastSwapTime(const ProcessNode* process_node, base::TimeTicks time);
void PrintAllSwapMetrics();
std::unique_ptr<base::RepeatingTimer> metrics_timer_ =
std::make_unique<base::RepeatingTimer>();
base::WeakPtrFactory<UserspaceSwapPolicy> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(UserspaceSwapPolicy);
};
} // namespace policies
} // namespace performance_manager
#endif // CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_USERSPACE_SWAP_POLICY_CHROMEOS_H_
// Copyright 2020 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.
#include "chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.h"
#include "base/allocator/buildflags.h"
#include "base/task/post_task.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/performance_manager/policies/policy_features.h"
#include "chrome/common/performance_manager/mojom/tcmalloc.mojom.h"
#include "chromeos/memory/userspace_swap/userspace_swap.h"
#include "components/performance_manager/graph/graph_impl_operations.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/performance_manager/test_support/mock_graphs.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_render_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace performance_manager {
namespace policies {
namespace {
using chromeos::memory::userspace_swap::UserspaceSwapConfig;
using testing::_;
using testing::Invoke;
using testing::Return;
using testing::StrictMock;
class MockUserspaceSwapPolicy : public UserspaceSwapPolicy {
public:
MockUserspaceSwapPolicy() : UserspaceSwapPolicy(InitTestConfig()) {}
~MockUserspaceSwapPolicy() override {}
MOCK_METHOD0(SwapNodesOnGraph, void(void));
MOCK_METHOD1(InitializeProcessNode, bool(const ProcessNode*));
MOCK_METHOD2(IsEligibleToSwap, bool(const ProcessNode*, const PageNode*));
MOCK_METHOD1(SwapProcessNode, void(const ProcessNode*));
MOCK_METHOD0(GetSwapDeviceFreeSpaceBytes, uint64_t(void));
MOCK_METHOD0(GetTotalSwapFileUsageBytes, uint64_t(void));
MOCK_METHOD1(GetProcessNodeSwapFileUsageBytes, uint64_t(const ProcessNode*));
MOCK_METHOD1(IsPageNodeAudible, bool(const PageNode*));
MOCK_METHOD1(IsPageNodeVisible, bool(const PageNode*));
MOCK_METHOD1(IsPageNodeLoading, bool(const PageNode*));
MOCK_METHOD1(GetTimeSinceLastVisibilityChange,
base::TimeDelta(const PageNode*));
// Allow our mock to dispatch to default implementations.
bool DefaultIsEligibleToSwap(const ProcessNode* process_node,
const PageNode* page_node) {
return UserspaceSwapPolicy::IsEligibleToSwap(process_node, page_node);
}
bool DefaultInitializeProcessNode(const ProcessNode* process_node) {
return UserspaceSwapPolicy::InitializeProcessNode(process_node);
}
void DefaultSwapNodesOnGraph() {
return UserspaceSwapPolicy::SwapNodesOnGraph();
}
base::MemoryPressureListener& listener() {
return memory_pressure_listener_.value();
}
base::TimeTicks get_last_graph_walk() { return last_graph_walk_; }
void set_last_graph_walk(base::TimeTicks t) { last_graph_walk_ = t; }
// We allow tests to modify the config for testing individual behaviors.
UserspaceSwapConfig& config() { return test_config_; }
private:
const UserspaceSwapConfig& InitTestConfig() {
// Create a simple starting config that can be modified as needed for tests.
// NOTE: We only initialize the configuration options which are used by the
// policy.
memset(&test_config_, 0, sizeof(test_config_));
test_config_.enabled = true;
test_config_.graph_walk_frequency = base::TimeDelta::FromSeconds(10);
test_config_.invisible_time_before_swap = base::TimeDelta::FromSeconds(30);
test_config_.process_swap_frequency = base::TimeDelta::FromSeconds(60);
test_config_.swap_on_freeze = true;
test_config_.swap_on_moderate_pressure = true;
return test_config_;
}
UserspaceSwapConfig test_config_ = {};
DISALLOW_COPY_AND_ASSIGN(MockUserspaceSwapPolicy);
};
class UserspaceSwapPolicyTest : public ::testing::Test {
public:
UserspaceSwapPolicyTest()
: browser_env_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~UserspaceSwapPolicyTest() override {}
void SetUp() override {
CreateAndPassMockPolicy();
// Create a simple graph.
process_node_ = CreateNode<ProcessNodeImpl>();
page_node_ = CreateNode<PageNodeImpl>();
frame_node_ =
graph()->CreateFrameNodeAutoId(process_node().get(), page_node().get());
}
void AttachProcess() {
// Create a process so this process node doesn't bail on Process.IsValid();
process_node()->SetProcess(base::Process::Current(), base::Time::Now());
}
void TearDown() override {
base::RunLoop().RunUntilIdle();
policy_ = nullptr;
frame_node_.reset();
page_node_.reset();
process_node_.reset();
graph_.TearDown();
}
void CreateAndPassMockPolicy() {
// Add our mock policy to the graph.
std::unique_ptr<StrictMock<MockUserspaceSwapPolicy>> mock_policy(
new StrictMock<MockUserspaceSwapPolicy>);
policy_ = mock_policy.get();
graph()->PassToGraph(std::move(mock_policy));
}
MockUserspaceSwapPolicy* policy() { return policy_; }
// "borrowed" helper methods from the GraphTestHarness.
template <class NodeClass, typename... Args>
TestNodeWrapper<NodeClass> CreateNode(Args&&... args) {
return TestNodeWrapper<NodeClass>::Create(graph(),
std::forward<Args>(args)...);
}
TestGraphImpl* graph() { return &graph_; }
content::BrowserTaskEnvironment* browser_env() { return &browser_env_; }
TestNodeWrapper<ProcessNodeImpl>& process_node() { return process_node_; }
TestNodeWrapper<PageNodeImpl>& page_node() { return page_node_; }
TestNodeWrapper<FrameNodeImpl>& frame_node() { return frame_node_; }
void FastForwardBy(base::TimeDelta delta) {
browser_env()->FastForwardBy(delta);
}
void RunUntilIdle() { browser_env()->RunUntilIdle(); }
private:
content::BrowserTaskEnvironment browser_env_;
TestGraphImpl graph_;
MockUserspaceSwapPolicy* policy_ = nullptr; // Not owned.
TestNodeWrapper<ProcessNodeImpl> process_node_;
TestNodeWrapper<PageNodeImpl> page_node_;
TestNodeWrapper<FrameNodeImpl> frame_node_;
DISALLOW_COPY_AND_ASSIGN(UserspaceSwapPolicyTest);
};
// This test validates that we only initialize a ProcessNode once.
TEST_F(UserspaceSwapPolicyTest, ValidateInitializeProcessOnlyOnce) {
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get())).Times(1);
// Attaching is a lifecycle state change.
AttachProcess();
// And now force another life cycle state change.
process_node()->SetProcessExitStatus(0);
}
// This test validates that we only walk the graph under moderate pressure.
TEST_F(UserspaceSwapPolicyTest, ValidateGraphWalkFrequencyNoPressure) {
auto last_walk_time = base::TimeTicks::Now();
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().swap_on_moderate_pressure = true;
policy()->set_last_graph_walk(last_walk_time);
// SwapNodesOnGraph shouldn't be called.
EXPECT_CALL(*policy(), SwapNodesOnGraph()).Times(0);
// We will fast forward by 100 graph walk frequencies, but since we're not
// under pressure we will expect no calls.
FastForwardBy(100 * policy()->config().graph_walk_frequency);
// Confirm through the last_graph_walk time that we didn't walk.
ASSERT_EQ(last_walk_time, policy()->get_last_graph_walk());
}
// This test validates that we only call WalkGraph every graph walk frequency
// seconds when under moderate pressure.
TEST_F(UserspaceSwapPolicyTest, ValidateGraphWalkFrequencyModeratePressure) {
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(60);
// We expect that we will call SwapNodesOnGraph only 2 times.
EXPECT_CALL(*policy(), SwapNodesOnGraph()).Times(2);
// Triger memory pressure and we should observe the walk since we've never
// walked before.
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
auto initial_walk_time = base::TimeTicks::Now();
FastForwardBy(base::TimeDelta::FromSeconds(1));
ASSERT_EQ(initial_walk_time, policy()->get_last_graph_walk());
// We will fast forward less than the graph walk frequency and confirm we
// don't walk again even when we receive another moderate pressure
// notification.
FastForwardBy(base::TimeDelta::FromSeconds(1));
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
// Since it's been less than the graph walk frequency we don't expect to walk.
ASSERT_EQ(initial_walk_time, policy()->get_last_graph_walk());
// Finally we will advance by a graph walk frequency and confirm we walk
// again.
FastForwardBy(policy()->config().graph_walk_frequency);
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
FastForwardBy(base::TimeDelta::FromSeconds(1));
ASSERT_NE(initial_walk_time, policy()->get_last_graph_walk());
}
// Validate we don't swap when not eligible.
TEST_F(UserspaceSwapPolicyTest, OnlySwapWhenEligibleToSwap) {
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(60);
// Dispatch to the default swap nodes on graph implementation.
EXPECT_CALL(*policy(), SwapNodesOnGraph())
.WillOnce(
Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));
// We will say this node is not eligible to swap.
EXPECT_CALL(*policy(),
IsEligibleToSwap(process_node().get(), page_node().get()))
.WillOnce(Return(false));
// And we will expect that SwapProcessNode is NOT called.
EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(0);
// Trigger moderate memory pressure to start the graph walk.
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
FastForwardBy(base::TimeDelta::FromSeconds(1));
}
TEST_F(UserspaceSwapPolicyTest, OnlySwapWhenEligibleToSwapTrue) {
// Dispatch to the default swap nodes on graph implementation.
EXPECT_CALL(*policy(), SwapNodesOnGraph())
.WillOnce(
Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));
// We will say this node is eligible to swap.
EXPECT_CALL(*policy(),
IsEligibleToSwap(process_node().get(), page_node().get()))
.WillOnce(Return(true));
// And we will expect that SwapProcessNode is called.
EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(1);
// Trigger moderate memory pressure to start the graph walk.
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
FastForwardBy(base::TimeDelta::FromSeconds(1));
}
// This test validates that we won't swap a node when it's visible.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenVisible) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
.WillOnce(Return(false));
// Because this page node is visible it should not be eligible for swap.
EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
.WillOnce(Return(true));
EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
page_node().get()));
}
// This test validates that we won't swap a node when it's audible.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenAudible) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
.WillOnce(Return(false));
EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
.WillOnce(Return(false));
// Because this page node is audible it won't be swappable.
EXPECT_CALL(*policy(), IsPageNodeAudible(page_node().get()))
.WillOnce(Return(true));
EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
page_node().get()));
}
// This test validates that we won't swap a node when it's loading.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenLoading) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
// Because this page node is loading it should not be eligible for swap.
EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
.WillOnce(Return(true));
EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
page_node().get()));
}
// This test validates that we do not swap an individual process node more than
// the configuration allows.
TEST_F(UserspaceSwapPolicyTest, ValidateProcessSwapFrequency) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency =
3 * policy()->config().graph_walk_frequency;
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
// Make sure this node is not visible, audible, or loading, and has been
// invisible for a long time because this test isn't validating those things.
EXPECT_CALL(*policy(), IsPageNodeAudible(page_node().get()))
.WillRepeatedly(Return(false));
EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
.WillRepeatedly(Return(false));
EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
.WillRepeatedly(Return(false));
EXPECT_CALL(*policy(), GetTimeSinceLastVisibilityChange(page_node().get()))
.WillRepeatedly(Return(base::TimeDelta::Max()));
EXPECT_CALL(*policy(), SwapNodesOnGraph())
.WillRepeatedly(
Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));
// Invoke the standard IsEligibleToSwapChecks.
EXPECT_CALL(*policy(),
IsEligibleToSwap(process_node().get(), page_node().get()))
.WillRepeatedly(
Invoke(policy(), &MockUserspaceSwapPolicy::DefaultIsEligibleToSwap));
// And although we will repeatedly walk the graph, we should only attempt to
// swap this process node exactly one time.
EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(1);
// We will walk the graph 3 times but this should only result in a single
// swap.
for (int i = 0; i < 3; ++i) {
FastForwardBy(policy()->config().graph_walk_frequency);
policy()->listener().SimulatePressureNotification(
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
}
}
// This test validates that we won't swap a node when the available swap space
// is below the limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenDiskSpaceTooLow) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().minimum_swap_disk_space_available = 1 << 30; // 1 GB
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
// We will have our mock return that there is only 100MB of disk space
// available. This should prevent swapping because it's below the minimum
// value.
EXPECT_CALL(*policy(), GetSwapDeviceFreeSpaceBytes())
.WillOnce(Return(100 << 20)); // 100MB
// Because GetSwapDeviceFreeSpaceBytes is less than the configured minimum it
// should return false.
EXPECT_FALSE(
policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}
// This test will validate that we won't swap a renderer that is already
// exceeding the individual renderer limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenPerRendererSwapExceeded) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().renderer_maximum_disk_swap_file_size_bytes =
128 << 20; // 128MB
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
// We will have our mock return that there is only 100MB of disk space
// available. This should prevent swapping because it's below the minimum
// value.
EXPECT_CALL(*policy(), GetProcessNodeSwapFileUsageBytes(process_node().get()))
.WillOnce(Return(190 << 20)); // 190 MB
// We're already using 190 MB which is more than the configured 128 MB so it
// should not be eligible to swap.
EXPECT_FALSE(
policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}
// This test will validate that we don't swap renderers when we've exceeded the
// global limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenTotalRendererSwapExceeded) {
// We will only swap a renderer once every 3 graph walks.
policy()->config().graph_walk_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().process_swap_frequency = base::TimeDelta::FromSeconds(1);
policy()->config().maximum_swap_disk_space_bytes = 1 << 30; // 1 GB
EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
.WillOnce(Return(true));
AttachProcess();
// We will have our mock return that there is only 100MB of disk space
// available. This should prevent swapping because it's below the minimum
// value.
EXPECT_CALL(*policy(), GetTotalSwapFileUsageBytes())
.WillOnce(Return(500 << 20)) // 500 MB
.WillOnce(Return(1200 << 20)); // 1.2 GB
// Since we're now below the 1GB limit we expect it will succeed.
EXPECT_TRUE(policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
// And on the second call we will expect that we will no longer be eligible
// because we've exceeded 1GB.
EXPECT_FALSE(
policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}
} // namespace
} // namespace policies
} // namespace performance_manager
......@@ -3340,6 +3340,7 @@ test("unit_tests") {
"../browser/performance_manager/observers/isolation_context_metrics_unittest.cc",
"../browser/performance_manager/observers/metrics_collector_unittest.cc",
"../browser/performance_manager/policies/dynamic_tcmalloc_policy_chromeos_unittest.cc",
"../browser/performance_manager/policies/userspace_swap_policy_chromeos_unittest.cc",
"../browser/performance_manager/policies/working_set_trimmer_policy_chromeos_unittest.cc",
"../browser/performance_manager/policies/working_set_trimmer_policy_unittest.cc",
"../browser/performance_manager/test_support/page_aggregator.cc",
......@@ -3704,6 +3705,7 @@ test("unit_tests") {
}
if (is_chromeos) {
sources += [
"../../chromeos/memory/userspace_swap/userspace_swap.cc",
"../browser/device_identity/chromeos/device_oauth2_token_store_chromeos_unittest.cc",
"../browser/ui/webui/certificate_provisioning_ui_handler_unittest.cc",
"../browser/ui/webui/chromeos/add_supervision/add_supervision_handler_utils_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment