chromium/chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.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/system/sys_info.h"
#include "base/time/time.h"
#include "chrome/browser/performance_manager/mechanisms/userspace_swap_chromeos.h"
#include "chromeos/ash/components/memory/userspace_swap/swap_storage.h"
#include "chromeos/ash/components/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 ::ash::memory::userspace_swap::SwapFile;
using ::ash::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_;
};

constexpr base::TimeDelta kMetricsInterval = base::Seconds(30);

}  // namespace

UserspaceSwapPolicy::UserspaceSwapPolicy(const UserspaceSwapConfig& config)
    : config_(config) {
  // To avoid failures related to chromeos-linux, we validate that we're running
  // on chromeos before enforcing the following check.
  if (base::SysInfo::IsRunningOnChromeOS()) {
    DCHECK(UserspaceSwapPolicy::UserspaceSwapSupportedAndEnabled());
  }

  if (VLOG_IS_ON(1) && !metrics_timer_->IsRunning()) {
    metrics_timer_->Start(
        FROM_HERE, kMetricsInterval,
        base::BindRepeating(&UserspaceSwapPolicy::PrintAllSwapMetrics,
                            weak_factory_.GetWeakPtr()));
  }
}

UserspaceSwapPolicy::UserspaceSwapPolicy()
    : UserspaceSwapPolicy(UserspaceSwapConfig::Get()) {}

UserspaceSwapPolicy::~UserspaceSwapPolicy() = default;

void UserspaceSwapPolicy::OnPassedToGraph(Graph* graph) {
  graph->AddProcessNodeObserver(this);

  // Only handle the memory pressure notifications if the feature to swap on
  // moderate pressure is enabled.
  if (config_->swap_on_moderate_pressure) {
    graph->AddSystemNodeObserver(this);
  }
}

void UserspaceSwapPolicy::OnTakenFromGraph(Graph* graph) {
  if (config_->swap_on_moderate_pressure) {
    graph->RemoveSystemNodeObserver(this);
  }

  graph->RemoveProcessNodeObserver(this);
}

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) << "rphid: " << process_node->GetRenderProcessHostId()
              << " 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): Add policy specific initialization or remove once final CLs
  // land.
  return true;
}

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);

    LOG_IF(ERROR, !data->process_initialized_)
        << "Unable to initialize process node";
  }
}

base::TimeTicks UserspaceSwapPolicy::GetLastSwapTime(
    const ProcessNode* process_node) {
  return UserspaceSwapPolicyData::EnsureForProcess(process_node)->last_swap_;
}

void UserspaceSwapPolicy::OnMemoryPressure(
    base::MemoryPressureListener::MemoryPressureLevel new_level) {
  if (new_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 : GetOwningGraph()->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) << "rphid: " << process_node->GetRenderProcessHostId()
              << " 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 : GetOwningGraph()->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) {
  performance_manager::mechanism::userspace_swap::SwapProcessNode(process_node);
}

uint64_t UserspaceSwapPolicy::GetProcessNodeSwapFileUsageBytes(
    const ProcessNode* process_node) {
  return performance_manager::mechanism::userspace_swap::
      GetProcessNodeSwapFileUsageBytes(process_node);
}

uint64_t UserspaceSwapPolicy::GetProcessNodeReclaimedBytes(
    const ProcessNode* process_node) {
  return performance_manager::mechanism::userspace_swap::
      GetProcessNodeReclaimedBytes(process_node);
}

uint64_t UserspaceSwapPolicy::GetTotalSwapFileUsageBytes() {
  return performance_manager::mechanism::userspace_swap::
      GetTotalSwapFileUsageBytes();
}

uint64_t UserspaceSwapPolicy::GetSwapDeviceFreeSpaceBytes() {
  return performance_manager::mechanism::userspace_swap::
      GetSwapDeviceFreeSpaceBytes();
}

bool UserspaceSwapPolicy::IsPageNodeLoadingOrBusy(const PageNode* page_node) {
  const PageNode::LoadingState loading_state = page_node->GetLoadingState();
  return loading_state == PageNode::LoadingState::kLoading ||
         loading_state == PageNode::LoadingState::kLoadedBusy;
}

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_) {
    return false;
  }

  // Always check with the mechanism to make sure that it can still be swapped
  // and that nothing unexpected has happened.
  if (!performance_manager::mechanism::userspace_swap::IsEligibleToSwap(
          process_node)) {
    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 (IsPageNodeLoadingOrBusy(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) {
    // Check if we can't swap because the device is running low on space.
    if (GetSwapDeviceFreeSpaceBytes() <
        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() {
  return ash::memory::userspace_swap::UserspaceSwapSupportedAndEnabled();
}

}  // namespace policies
}  // namespace performance_manager