chromium/chrome/browser/performance_manager/mechanisms/working_set_trimmer_chromeos_unittest.cc

// Copyright 2021 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/mechanisms/working_set_trimmer_chromeos.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "ash/components/arc/arc_features.h"
#include "ash/components/arc/memory/arc_memory_bridge.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_arc_session.h"
#include "ash/components/arc/test/fake_memory_instance.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/timer/elapsed_timer.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/arc/test/test_arc_session_manager.h"
#include "chrome/browser/ash/arc/vmm/arcvm_working_set_trim_executor.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace performance_manager {
namespace mechanism {

class TestWorkingSetTrimmerChromeOS : public testing::Test {
 public:
  TestWorkingSetTrimmerChromeOS()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  TestWorkingSetTrimmerChromeOS(const TestWorkingSetTrimmerChromeOS&) = delete;
  TestWorkingSetTrimmerChromeOS& operator=(
      const TestWorkingSetTrimmerChromeOS&) = delete;
  ~TestWorkingSetTrimmerChromeOS() override = default;

  void SetUp() override {
    ash::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr);
    arc_session_manager_ = arc::CreateTestArcSessionManager(
        std::make_unique<arc::ArcSessionRunner>(
            base::BindRepeating(arc::FakeArcSession::Create)));
    testing_profile_ = std::make_unique<TestingProfile>();
    CreateTrimmer(testing_profile_.get());
    arc::ArcMemoryBridge::GetForBrowserContextForTesting(
        testing_profile_.get());

    // Set a fake memory instance so that DropCaches() calls in test will
    // succeed.
    arc::ArcServiceManager::Get()->arc_bridge_service()->memory()->SetInstance(
        &memory_instance_);
    arc::WaitForInstanceReady(
        arc::ArcServiceManager::Get()->arc_bridge_service()->memory());
  }

  void TearDown() override {
    trimmer_.reset();
    testing_profile_.reset();
    TearDownArcSessionManager();
    ash::ConciergeClient::Shutdown();
  }

 protected:
  void CreateTrimmer(content::BrowserContext* context) {
    trimmer_ = WorkingSetTrimmerChromeOS::CreateForTesting(context);
  }
  void TrimArcVmWorkingSet(
      WorkingSetTrimmerChromeOS::TrimArcVmWorkingSetCallback callback) {
    trimmer_->TrimArcVmWorkingSet(std::move(callback),
                                  ArcVmReclaimType::kReclaimAll,
                                  arc::ArcSession::kNoPageLimit);
  }
  void TrimArcVmWorkingSetDropPageCachesOnly(
      WorkingSetTrimmerChromeOS::TrimArcVmWorkingSetCallback callback) {
    trimmer_->TrimArcVmWorkingSet(std::move(callback),
                                  ArcVmReclaimType::kReclaimGuestPageCaches,
                                  arc::ArcSession::kNoPageLimit);
  }
  void TrimArcVmWorkingSetWithPageLimit(
      WorkingSetTrimmerChromeOS::TrimArcVmWorkingSetCallback callback,
      int page_limit) {
    trimmer_->TrimArcVmWorkingSet(std::move(callback),
                                  ArcVmReclaimType::kReclaimAll, page_limit);
  }

  void TearDownArcSessionManager() { arc_session_manager_.reset(); }

  arc::FakeMemoryInstance* memory_instance() { return &memory_instance_; }

  arc::ArcSessionRunner* arc_session_runner() {
    return arc_session_manager_->GetArcSessionRunnerForTesting();
  }

  static constexpr char kDefaultLocale[] = "en-US";
  arc::UpgradeParams DefaultUpgradeParams() {
    arc::UpgradeParams params;
    params.locale = kDefaultLocale;
    return params;
  }

  // Use this object within a code block that needs to interact with
  // the FakeSession within the ArcSessionRunner.
  // It is important to discard the session when done, even if errors
  // happen - so doing it in the destructor, to make it automatic.
  struct FakeArcSessionHolder {
    explicit FakeArcSessionHolder(arc::ArcSessionRunner* runner)
        : runner_(runner) {
      runner_->MakeArcSessionForTesting();
    }
    FakeArcSessionHolder(const FakeArcSessionHolder&) = delete;
    FakeArcSessionHolder& operator=(const FakeArcSessionHolder&) = delete;
    ~FakeArcSessionHolder() { runner_->DiscardArcSessionForTesting(); }
    arc::FakeArcSession* session() {
      return static_cast<arc::FakeArcSession*>(
          runner_->GetArcSessionForTesting());
    }
    raw_ptr<arc::ArcSessionRunner> runner_;
  };

  content::BrowserTaskEnvironment& task_environment() {
    return task_environment_;
  }

  std::unique_ptr<WorkingSetTrimmerChromeOS> trimmer_;

 private:
  content::BrowserTaskEnvironment task_environment_;
  arc::ArcServiceManager arc_service_manager_;
  arc::FakeMemoryInstance memory_instance_;
  std::unique_ptr<arc::ArcSessionManager> arc_session_manager_;
  std::unique_ptr<TestingProfile> testing_profile_;
};

namespace {

// Tests that TrimArcVmWorkingSet runs the passed callback,
// and that the page limit is passed as requested.
TEST_F(TestWorkingSetTrimmerChromeOS, TrimArcVmWorkingSet) {
  std::optional<bool> result;
  std::string reason;

  {
    FakeArcSessionHolder session_holder(arc_session_runner());
    session_holder.session()->set_trim_result(true, "test_reason");
    TrimArcVmWorkingSetWithPageLimit(
        base::BindLambdaForTesting(
            [&result, &reason](bool disposition, const std::string& status) {
              result = disposition;
              reason = status;
            }),
        5003);
    base::RunLoop().RunUntilIdle();
    ASSERT_TRUE(result);
    EXPECT_TRUE(*result);
    EXPECT_EQ(reason, "test_reason");
    EXPECT_EQ(session_holder.session()->trim_vm_memory_count(), 1);
    EXPECT_EQ(session_holder.session()->last_trim_vm_page_limit(), 5003);
  }
}

// Tests that TrimArcVmWorkingSet runs the passed callback even when
// BrowserContext is not available.
TEST_F(TestWorkingSetTrimmerChromeOS, TrimArcVmWorkingSetNoBrowserContext) {
  // Create a trimmer again with a null BrowserContext to make it unavailable.
  CreateTrimmer(nullptr);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
}

// Tests that TrimArcVmWorkingSet runs the passed callback even when
// ArcMemoryBridge is not available.
TEST_F(TestWorkingSetTrimmerChromeOS, TrimArcVmWorkingSetNoArcMemoryBridge) {
  // Create a trimmer again with a different profile (BrowserContext) to make
  // ArcMemoryBridge unavailable.
  TestingProfile another_profile;
  CreateTrimmer(&another_profile);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);

  trimmer_.reset();
}

// Tests that TrimArcVmWorkingSet runs the passed callback even when
// ArcSessionManager is not available.
TEST_F(TestWorkingSetTrimmerChromeOS, TrimArcVmWorkingSetNoArcSessionManager) {
  // Make ArcSessionManager unavailable.
  TearDownArcSessionManager();

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  EXPECT_FALSE(*result);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestReclaimEnabled_Success) {
  base::HistogramTester histogram_tester;
  base::ScopedMockElapsedTimersForTest mock_elapsed_timers;
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  feature_list.InitAndEnableFeatureWithParameters(arc::kGuestSwap, params);
  memory_instance()->set_reclaim_all_result(2, 1);
  // Making arc session trim result to be false to be sure it's not being used.
  FakeArcSessionHolder session_holder(arc_session_runner());
  session_holder.session()->set_trim_result(false, "test_reason");

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  histogram_tester.ExpectUniqueSample("Arc.GuestZram.SuccessfulReclaim", 1, 1);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.ReclaimedProcess", 2, 1);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.UnreclaimedProcess", 1, 1);
  histogram_tester.ExpectUniqueTimeSample(
      "Arc.GuestZram.TotalReclaimTime",
      base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestReclaimEnabled_Failure) {
  base::HistogramTester histogram_tester;
  base::ScopedMockElapsedTimersForTest mock_elapsed_timers;
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  feature_list.InitAndEnableFeatureWithParameters(arc::kGuestSwap, params);
  memory_instance()->set_reclaim_all_result(0, 0);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  histogram_tester.ExpectUniqueSample("Arc.GuestZram.SuccessfulReclaim", 0, 1);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.ReclaimedProcess", 0, 0);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.UnreclaimedProcess", 0, 0);
  histogram_tester.ExpectUniqueTimeSample(
      "Arc.GuestZram.TotalReclaimTime",
      base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 0);
  ASSERT_TRUE(result);
  ASSERT_FALSE(*result);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestReclaimEnabled_AnonPagesOnly) {
  base::HistogramTester histogram_tester;
  base::ScopedMockElapsedTimersForTest mock_elapsed_timers;
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  params["guest_reclaim_only_anonymous"] = "true";
  feature_list.InitAndEnableFeatureWithParameters(arc::kGuestSwap, params);
  memory_instance()->set_reclaim_all_result(0, 0);
  memory_instance()->set_reclaim_anon_result(2, 0);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  histogram_tester.ExpectUniqueSample("Arc.GuestZram.SuccessfulReclaim", 1, 1);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.ReclaimedProcess", 2, 1);
  histogram_tester.ExpectUniqueSample("Arc.GuestZram.UnreclaimedProcess", 0, 1);
  histogram_tester.ExpectUniqueTimeSample(
      "Arc.GuestZram.TotalReclaimTime",
      base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestVirtualSwap_GuestReclaimSucceeded) {
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  params["virtual_swap_enabled"] = "true";
  feature_list.InitWithFeaturesAndParameters({{arc::kGuestSwap, params}},
                                             {arc::kLockGuestMemory});
  FakeArcSessionHolder session_holder(arc_session_runner());
  session_holder.session()->set_trim_result(true, "");
  // Guest reclaimed succeeded.
  memory_instance()->set_reclaim_all_result(2, 1);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
  // Host reclaim should be invoked with virtual swap
  ASSERT_EQ(session_holder.session()->trim_vm_memory_count(), 1);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestVirtualSwap_GuestReclaimFailed) {
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  params["virtual_swap_enabled"] = "true";
  feature_list.InitWithFeaturesAndParameters({{arc::kGuestSwap, params}},
                                             {arc::kLockGuestMemory});
  FakeArcSessionHolder session_holder(arc_session_runner());
  session_holder.session()->set_trim_result(true, "");
  // Guest reclaimed failed.
  memory_instance()->set_reclaim_all_result(0, 2);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
  // Host reclaim should be invoked with virtual swap
  ASSERT_EQ(session_holder.session()->trim_vm_memory_count(), 1);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestVirtualSwap_GuestMemoryLocked) {
  base::test::ScopedFeatureList feature_list;
  base::FieldTrialParams params;
  params["guest_reclaim_enabled"] = "true";
  params["virtual_swap_enabled"] = "true";
  feature_list.InitWithFeaturesAndParameters(
      {{arc::kGuestSwap, params}, {arc::kLockGuestMemory, {}}}, {});
  FakeArcSessionHolder session_holder(arc_session_runner());
  // Guest reclaimed succeeded.
  memory_instance()->set_reclaim_all_result(2, 0);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
  // Host reclaim should not happen when guest memory is locked
  ASSERT_EQ(session_holder.session()->trim_vm_memory_count(), 0);
}

TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSet_GuestZramDisabled_ArcSessionIsUsed) {
  FakeArcSessionHolder session_holder(arc_session_runner());
  session_holder.session()->set_trim_result(true, "");
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(arc::kGuestSwap);
  // If memory_instance is used then the trim operation should fail.
  memory_instance()->set_reclaim_all_result(0, 0);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
}

TEST_F(
    TestWorkingSetTrimmerChromeOS,
    TrimArcVmWorkingSet_GuestZramEnabledWithNoGuestReclaim_ArcSessionIsUsed) {
  FakeArcSessionHolder session_holder(arc_session_runner());
  session_holder.session()->set_trim_result(true, "");
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(arc::kGuestSwap);
  // If memory_instance is used then the trim operation should fail.
  memory_instance()->set_reclaim_all_result(0, 0);

  std::optional<bool> result;
  TrimArcVmWorkingSet(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(result);
  ASSERT_TRUE(*result);
}

// Tests that TrimArcVmWorkingSetDropPageCachesOnly runs the passed callback.
TEST_F(TestWorkingSetTrimmerChromeOS, TrimArcVmWorkingSetDropPageCachesOnly) {
  std::optional<bool> result;
  TrimArcVmWorkingSetDropPageCachesOnly(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  EXPECT_TRUE(*result);
}

// Tests that TrimArcVmWorkingSetDropPageCachesOnly runs the passed callback
// with false (failure) when DropCaches() fails.
TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSetDropPageCachesOnly_DropCachesFailure) {
  // Inject the failure.
  memory_instance()->set_drop_caches_result(false);

  std::optional<bool> result;
  TrimArcVmWorkingSetDropPageCachesOnly(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  EXPECT_FALSE(*result);
}

// Tests that TrimArcVmWorkingSetDropPageCachesOnly runs the passed callback
// even when BrowserContext is not available.
TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSetDropPageCachesOnly_NoBrowserContext) {
  // Create a trimmer again with a null BrowserContext to make it unavailable.
  CreateTrimmer(nullptr);

  std::optional<bool> result;
  TrimArcVmWorkingSetDropPageCachesOnly(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  // Expect false because dropping caches is not possible without a browser
  // context.
  EXPECT_FALSE(*result);
}

// Tests that TrimArcVmWorkingSetDropPageCachesOnly runs the passed callback
// even when ArcMemoryBridge is not available.
TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSetDropPageCachesOnly_NoArcMemoryBridge) {
  // Create a trimmer again with a different profile (BrowserContext) to make
  // ArcMemoryBridge unavailable.
  TestingProfile another_profile;
  CreateTrimmer(&another_profile);

  std::optional<bool> result;
  TrimArcVmWorkingSetDropPageCachesOnly(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  // Expect false because dropping caches is not possible without a memory
  // bridge.
  EXPECT_FALSE(*result);

  trimmer_.reset();
}

// Tests that TrimArcVmWorkingSetDropPageCachesOnly runs the passed callback
// even when ArcSessionManager is not available.
TEST_F(TestWorkingSetTrimmerChromeOS,
       TrimArcVmWorkingSetDropPageCachesOnly_NoArcSessionManager) {
  // Make ArcSessionManager unavailable.
  TearDownArcSessionManager();

  std::optional<bool> result;
  TrimArcVmWorkingSetDropPageCachesOnly(base::BindLambdaForTesting(
      [&result](bool r, const std::string&) { result = r; }));
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(result);
  // Expect true because dropping caches can be done without ArcSessionManager.
  // The manager is necessary only for the actual VM trimming.
  EXPECT_TRUE(*result);
}

}  // namespace
}  // namespace mechanism
}  // namespace performance_manager