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

// Copyright 2022 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/oom_score_policy_chromeos.h"

#include <algorithm>
#include <set>

#include "base/process/memory.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/process_node.h"
#include "content/public/common/content_constants.h"

namespace performance_manager::policies {

namespace {

// TODO(crbug.com/40283990): oom_score_adj of some processes could be
// out-of-dated. Override OnIsFocusedChanged to update the oom_score_adj of the
// focused tab to make it harder to be killed by the Linux oom killer.
constexpr base::TimeDelta kOomScoresAssignmentMinimalInterval =
    base::Seconds(10);

}  // namespace

OomScorePolicyChromeOS::OomScorePolicyChromeOS() = default;
OomScorePolicyChromeOS::~OomScorePolicyChromeOS() = default;

void OomScorePolicyChromeOS::OnPassedToGraph(Graph* graph) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  graph->AddPageNodeObserver(this);
}

void OomScorePolicyChromeOS::OnTakenFromGraph(Graph* graph) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  graph->RemovePageNodeObserver(this);
}

void OomScorePolicyChromeOS::OnPageNodeAdded(const PageNode* page_node) {
  HandlePageNodeEventsThrottled();
}

void OomScorePolicyChromeOS::OnBeforePageNodeRemoved(
    const PageNode* page_node) {
  HandlePageNodeEventsThrottled();
}

void OomScorePolicyChromeOS::OnIsFocusedChanged(const PageNode* page_node) {
  HandlePageNodeEventsThrottled();
}

void OomScorePolicyChromeOS::OnIsVisibleChanged(const PageNode* page_node) {
  HandlePageNodeEventsThrottled();
}

void OomScorePolicyChromeOS::OnTypeChanged(const PageNode* page_node,
                                           PageType previous_type) {
  HandlePageNodeEventsThrottled();
}

void OomScorePolicyChromeOS::HandlePageNodeEventsThrottled() {
  base::TimeTicks now = base::TimeTicks::Now();
  if (now - last_oom_scores_assignment_ > kOomScoresAssignmentMinimalInterval) {
    last_oom_scores_assignment_ = now;
    HandlePageNodeEvents();
  }
}

void OomScorePolicyChromeOS::HandlePageNodeEvents() {
  PageDiscardingHelper* discarding_helper =
      PageDiscardingHelper::GetFromGraph(GetOwningGraph());

  Graph::NodeSetView<const PageNode*> all_page_nodes =
      GetOwningGraph()->GetAllPageNodes();
  std::vector<PageNodeSortProxy> candidates;
  candidates.reserve(all_page_nodes.size());

  for (const PageNode* page_node : all_page_nodes) {
    PageDiscardingHelper::CanDiscardResult can_discard_result =
        discarding_helper->CanDiscard(
            page_node, PageDiscardingHelper::DiscardReason::URGENT);
    bool is_marked =
        (can_discard_result == PageDiscardingHelper::CanDiscardResult::kMarked);
    bool is_protected = (can_discard_result ==
                         PageDiscardingHelper::CanDiscardResult::kProtected);
    bool is_visible = page_node->IsVisible();
    bool is_focused = page_node->IsFocused();
    candidates.emplace_back(page_node, is_marked, is_visible, is_protected,
                            is_focused,
                            page_node->GetTimeSinceLastVisibilityChange());
  }

  // Sorts with descending importance.
  std::sort(candidates.begin(), candidates.end(),
            [](const PageNodeSortProxy& lhs, const PageNodeSortProxy& rhs) {
              return rhs < lhs;
            });

  oom_score_map_ = DistributeOomScore(candidates);
}

OomScorePolicyChromeOS::ProcessScoreMap
OomScorePolicyChromeOS::DistributeOomScore(
    const std::vector<PageNodeSortProxy>& candidates) {
  ProcessScoreMap score_map;

  std::vector<base::ProcessId> pids = GetUniquePids(candidates);

  const int pid_count = pids.size();
  if (pid_count == 0) {
    return score_map;
  }

  // Now we distribute oom_score_adj evenly in the range based on the sorted
  // list. We're assigning priorities in the range of kLowestRendererOomScore
  // to kHighestRendererOomScore (defined in chrome_constants.h). oom_score_adj
  // takes values from -1000 to 1000. Negative values are reserved for system
  // processes. Higher values are more likely to be killed by the Linux kernel
  // OOM killer.
  const float range = static_cast<float>(content::kHighestRendererOomScore -
                                         content::kLowestRendererOomScore);
  const float adj_increment = (pid_count == 1) ? 0 : range / (pid_count - 1);

  float adj_raw = content::kLowestRendererOomScore;
  for (base::ProcessId pid : pids) {
    const int adj = round(adj_raw);

    score_map[pid] = adj;

    if (GetCachedOomScore(pid) != adj) {
      VLOG(3) << "Update OOM score " << adj << " for " << pid;
      if (!base::AdjustOOMScore(pid, adj)) {
        LOG(ERROR) << "Failed to set oom_score_adj to " << adj
                   << " for process " << pid;
      }
    }
    adj_raw += adj_increment;
  }

  return score_map;
}

std::vector<base::ProcessId> OomScorePolicyChromeOS::GetUniquePids(
    const std::vector<PageNodeSortProxy>& candidates) {
  std::vector<base::ProcessId> pids;

  std::set<base::ProcessId> pid_set;

  for (const auto& candidate : candidates) {
    const FrameNode* main_frame_node =
        candidate.page_node()->GetMainFrameNode();
    if (!main_frame_node) {
      continue;
    }
    base::ProcessId pid = main_frame_node->GetProcessNode()->GetProcessId();

    if (pid == base::kNullProcessId || pid_set.find(pid) != pid_set.end()) {
      continue;
    }

    pids.push_back(pid);
    pid_set.insert(pid);
  }

  return pids;
}

int OomScorePolicyChromeOS::GetCachedOomScore(base::ProcessHandle pid) {
  auto it = oom_score_map_.find(pid);
  if (it == oom_score_map_.end()) {
    return -1;
  }
  return it->second;
}

}  // namespace performance_manager::policies