// Copyright 2025 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/termination_target_policy.h"

#include <algorithm>
#include <cstdint>

#include "base/byte_size.h"
#include "chrome/browser/performance_manager/policies/discard_eligibility_policy.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "content/public/common/process_type.h"

namespace performance_manager {

// Private memory footprint threshold above which a process is considered large.
constexpr base::ByteSize kLargeProcessFootprintThreshold = base::MiB(100);

namespace {

struct PotentialTerminationTarget {
  raw_ptr<const ProcessNode> process = nullptr;
  base::ByteSize private_footprint;
  bool can_discard = true;
  // Last time a page in the process was visible. `base::TimeTicks::Max()` if
  // currently visible. `base::TimeTicks()` if there is no visible page in the
  // process.
  base::TimeTicks last_visible_time;

  // Returns true if `this` is a better termination target than `other`.
  bool operator<(const PotentialTerminationTarget& other) const {
    if (is_large() != other.is_large()) {
      return is_large();
    }

    if (can_discard != other.can_discard) {
      return can_discard;
    }

    return last_visible_time < other.last_visible_time;
  }

  bool is_large() const {
    return private_footprint > kLargeProcessFootprintThreshold;
  }
};

}  // namespace

TerminationTargetPolicy::TerminationTargetPolicy(
    std::unique_ptr<TerminationTargetSetter> termination_target_setter)
    : termination_target_setter_(std::move(termination_target_setter)) {}

TerminationTargetPolicy::~TerminationTargetPolicy() {
  if (current_termination_target_) {
    termination_target_setter_->SetTerminationTarget(nullptr);
  }
}

void TerminationTargetPolicy::OnPassedToGraph(Graph* graph) {
  graph->AddSystemNodeObserver(this);
  graph->AddPageNodeObserver(this);
  graph->AddProcessNodeObserver(this);
}

void TerminationTargetPolicy::OnTakenFromGraph(Graph* graph) {
  graph->RemoveProcessNodeObserver(this);
  graph->RemovePageNodeObserver(this);
  graph->RemoveSystemNodeObserver(this);
}

void TerminationTargetPolicy::OnIsVisibleChanged(const PageNode* page_node) {
  UpdateTerminationTarget();
}

void TerminationTargetPolicy::OnPageNodeRemoved(const PageNode* page_node) {
  UpdateTerminationTarget();
}

void TerminationTargetPolicy::OnBeforeProcessNodeRemoved(
    const ProcessNode* process_node) {
  if (process_node == current_termination_target_) {
    UpdateTerminationTarget(/*process_being_deleted=*/process_node);
  }
}

void TerminationTargetPolicy::OnProcessLifetimeChange(
    const ProcessNode* process_node) {
  if (process_node == current_termination_target_) {
    UpdateTerminationTarget(process_node);
  }
}

void TerminationTargetPolicy::OnProcessMemoryMetricsAvailable(
    const SystemNode* system_node) {
  UpdateTerminationTarget();
}

void TerminationTargetPolicy::UpdateTerminationTarget(
    const ProcessNode* process_being_deleted) {
  std::vector<PotentialTerminationTarget> targets;
  auto process_nodes = GetOwningGraph()->GetAllProcessNodes();
  targets.reserve(process_nodes.size());

  policies::DiscardEligibilityPolicy* eligibility_policy =
      policies::DiscardEligibilityPolicy::GetFromGraph(GetOwningGraph());

  for (const ProcessNode* process_node : process_nodes) {
    if (process_node->GetProcessType() != content::PROCESS_TYPE_RENDERER) {
      continue;
    }

    if (process_node == process_being_deleted) {
      continue;
    }

    if (!process_node->GetProcess().IsValid()) {
      continue;
    }

    targets.emplace_back();
    PotentialTerminationTarget& target = targets.back();
    target.process = process_node;
    target.private_footprint = process_node->GetPrivateFootprint();

    for (const FrameNode* frame_node : process_node->GetFrameNodes()) {
      const PageNode* page_node = frame_node->GetPageNode();
      if (eligibility_policy->CanDiscard(
              page_node,
              policies::DiscardEligibilityPolicy::DiscardReason::URGENT,
              /*ignore_recent_visibility=*/true) !=
          policies::CanDiscardResult::kEligible) {
        target.can_discard = false;
      }
      if (page_node->IsVisible()) {
        target.last_visible_time = base::TimeTicks::Max();
      } else {
        target.last_visible_time = std::max(
            target.last_visible_time, page_node->GetLastVisibilityChangeTime());
      }
    }
  }

  auto min_element_it = std::min_element(targets.begin(), targets.end());
  if (min_element_it != targets.end()) {
    if (min_element_it->process != current_termination_target_) {
      termination_target_setter_->SetTerminationTarget(min_element_it->process);
      current_termination_target_ = min_element_it->process;
    }
  } else if (current_termination_target_) {
    termination_target_setter_->SetTerminationTarget(nullptr);
    current_termination_target_ = nullptr;
  }
}

}  // namespace performance_manager
