chromium/chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos_unittest.cc

// 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 "chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.h"

#include "base/allocator/buildflags.h"
#include "base/memory/raw_ptr.h"
#include "base/system/sys_info.h"
#include "chrome/browser/performance_manager/policies/policy_features.h"
#include "chromeos/ash/components/memory/userspace_swap/userspace_swap.h"
#include "components/performance_manager/graph/graph_impl.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 ::ash::memory::userspace_swap::UserspaceSwapConfig;
using testing::_;
using testing::Invoke;
using testing::Return;
using testing::StrictMock;

class MockUserspaceSwapPolicy : public UserspaceSwapPolicy {
 public:
  MockUserspaceSwapPolicy() : UserspaceSwapPolicy(InitTestConfig()) {}

  MockUserspaceSwapPolicy(const MockUserspaceSwapPolicy&) = delete;
  MockUserspaceSwapPolicy& operator=(const MockUserspaceSwapPolicy&) = delete;

  ~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::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::Seconds(10);
    test_config_.invisible_time_before_swap = base::Seconds(30);
    test_config_.process_swap_frequency = base::Seconds(60);
    test_config_.swap_on_freeze = true;
    test_config_.swap_on_moderate_pressure = true;
    return test_config_;
  }

  UserspaceSwapConfig test_config_ = {};
};

class UserspaceSwapPolicyTest : public ::testing::Test {
 public:
  UserspaceSwapPolicyTest()
      : browser_env_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  UserspaceSwapPolicyTest(const UserspaceSwapPolicyTest&) = delete;
  UserspaceSwapPolicyTest& operator=(const UserspaceSwapPolicyTest&) = delete;

  ~UserspaceSwapPolicyTest() override {}

  void SetUp() override {
    if (!base::SysInfo::IsRunningOnChromeOS()) {
      GTEST_SKIP() << "Skip test on chromeos-linux";
    }

    graph_ = std::make_unique<TestGraphImpl>();
    graph_->SetUp();

    CreateAndPassMockPolicy();

    // Create a simple graph.
    process_node_ = CreateNode<ProcessNodeImpl>();
    page_node_ = CreateNode<PageNodeImpl>();
    frame_node_ =
        graph()->CreateFrameNodeAutoId(process_node().get(), page_node().get());
    system_node_ = std::make_unique<TestNodeWrapper<SystemNodeImpl>>(
        TestNodeWrapper<SystemNodeImpl>::Create(graph()));
  }

  void AttachProcess() {
    // Create a process so this process node doesn't bail on Process.IsValid();
    process_node()->SetProcess(base::Process::Current(),
                               /* launch_time=*/base::TimeTicks::Now());
  }

  void TearDown() override {
    if (!base::SysInfo::IsRunningOnChromeOS()) {
      // Also skip TearDown() if SetUp() was skipped.
      return;
    }
    base::RunLoop().RunUntilIdle();

    policy_ = nullptr;
    frame_node_.reset();
    page_node_.reset();
    process_node_.reset();
    system_node_.reset();
    graph_->TearDown();
    graph_ = nullptr;
  }

  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_.get(); }
  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_; }
  TestNodeWrapper<SystemNodeImpl>& system_node() {
    return *(system_node_.get());
  }

  void FastForwardBy(base::TimeDelta delta) {
    browser_env()->FastForwardBy(delta);
  }

  void RunUntilIdle() { browser_env()->RunUntilIdle(); }

 private:
  content::BrowserTaskEnvironment browser_env_;
  std::unique_ptr<TestGraphImpl> graph_;
  raw_ptr<MockUserspaceSwapPolicy> policy_ = nullptr;  // Not owned.

  TestNodeWrapper<ProcessNodeImpl> process_node_;
  TestNodeWrapper<PageNodeImpl> page_node_;
  TestNodeWrapper<FrameNodeImpl> frame_node_;
  std::unique_ptr<TestNodeWrapper<SystemNodeImpl>> system_node_;
};

// 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::Seconds(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::Seconds(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.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  auto initial_walk_time = base::TimeTicks::Now();
  FastForwardBy(base::Seconds(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::Seconds(1));
  system_node()->OnMemoryPressureForTesting(
      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);
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);

  FastForwardBy(base::Seconds(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::Seconds(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.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  FastForwardBy(base::Seconds(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.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  FastForwardBy(base::Seconds(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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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::Seconds(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);
    system_node()->OnMemoryPressureForTesting(
        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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(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