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

// Copyright 2019 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/working_set_trimmer_policy_chromeos.h"

#include "ash/components/arc/arc_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/synchronization/lock.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/process/arc_process.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/arc/vmm/arcvm_working_set_trim_executor.h"
#include "chrome/browser/performance_manager/mechanisms/working_set_trimmer.h"
#include "chrome/browser/performance_manager/policies/policy_features.h"
#include "chrome/browser/performance_manager/policies/working_set_trimmer_policy_arcvm.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/performance_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"

namespace performance_manager {
namespace policies {

namespace {
// TODO(crbug.com/40755583): Remove the global static variable and make it
// GraphOwned once performance_manager code is migrated to UI thread.
WorkingSetTrimmerPolicyChromeOS::ArcVmDelegate* g_arcvm_delegate_for_testing =
    nullptr;

enum ArcProcessType { kApp, kSystem };
void GetArcProcessListOnUIThread(
    ArcProcessType type,
    base::WeakPtr<
        performance_manager::policies::WorkingSetTrimmerPolicyChromeOS> ptr,
    int processes_per_trim) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  arc::ArcProcessService* arc_process_service = arc::ArcProcessService::Get();
  if (!arc_process_service) {
    return;
  }

  // Now we need to bounce back to the PM sequence so we can do stuff with the
  // process list.
  auto callback = base::BindOnce(
      [](decltype(ptr) ptr, decltype(processes_per_trim) processes_per_trim,
         arc::ArcProcessService::OptionalArcProcessList opt_proc_list) {
        PerformanceManager::CallOnGraph(
            FROM_HERE,
            base::BindOnce(
                &WorkingSetTrimmerPolicyChromeOS::TrimReceivedArcProcesses, ptr,
                processes_per_trim, std::move(opt_proc_list)));
      },
      ptr, processes_per_trim);

  if (type == kApp) {
    arc_process_service->RequestAppProcessList(std::move(callback));
  } else if (type == kSystem) {
    arc_process_service->RequestSystemProcessList(std::move(callback));
  }
}

}  // namespace

WorkingSetTrimmerPolicyChromeOS::WorkingSetTrimmerPolicyChromeOS() {
  trim_on_freeze_ = base::FeatureList::IsEnabled(features::kTrimOnFreeze);
  trim_arc_on_memory_pressure_ =
      base::FeatureList::IsEnabled(features::kTrimArcOnMemoryPressure);
  trim_arcvm_on_memory_pressure_ =
      base::FeatureList::IsEnabled(features::kTrimArcVmOnMemoryPressure);
  disable_trim_while_suspended_ =
      base::FeatureList::IsEnabled(features::kDisableTrimmingWhileSuspended);

  params_ = features::TrimOnMemoryPressureParams::GetParams();

  if (trim_arc_on_memory_pressure_) {
    // Validate ARC parameters.
    if (!params_.trim_arc_app_processes && !params_.trim_arc_system_processes) {
      LOG(ERROR)
          << "Misconfiguration ARC trimming on memory pressure is enabled "
             "but both app and system process trimming are disabled.";
      trim_arc_on_memory_pressure_ = false;
    } else if (!arc::IsArcAvailable()) {
      DLOG(ERROR) << "ARC is not available";
      trim_arc_on_memory_pressure_ = false;
    } else if (arc::IsArcVmEnabled()) {
      // ARCVM is handled separately.
      trim_arc_on_memory_pressure_ = false;
    }
  }

  if (trim_arcvm_on_memory_pressure_) {
    if (!arc::IsArcAvailable() || !arc::IsArcVmEnabled()) {
      DLOG(ERROR) << "ARCVM is not available";
      trim_arcvm_on_memory_pressure_ = false;
    }
  }

  if (disable_trim_while_suspended_) {
    power_manager_observation_.Observe(chromeos::PowerManagerClient::Get());
  }
}

WorkingSetTrimmerPolicyChromeOS::~WorkingSetTrimmerPolicyChromeOS() = default;

// On MemoryPressure we will try to trim the working set of some renders if they
// have been backgrounded for some period of time and have not been trimmed for
// at least the backoff period.
void WorkingSetTrimmerPolicyChromeOS::OnMemoryPressure(
    base::MemoryPressureListener::MemoryPressureLevel level) {
  bool skip_trimming_due_to_suspend = false;
  if (disable_trim_while_suspended_) {
    base::TimeTicks now = base::TimeTicks::Now();
    base::AutoLock lock(mutex_);
    skip_trimming_due_to_suspend =
        is_system_suspended_ ||
        (last_suspend_done_time_ &&
         now - *last_suspend_done_time_ < params_.suspend_backoff_time);
  }
  // We define idle as the last visible time be greater than some threshold.
  // Since the monotonic clock can keep on ticking during suspend (by dark
  // resume) when we resume it can look like the tab has not been used in some
  // huge amount of time. In reality, the user hasn't been doing anything.
  // Waiting for kSuspendBackoffTimeSec after resuming ensures that enough time
  // has elapsed so that inappropriately added time from dark resume can no
  // longer affect whether or not a tab has been invisible for long enough to be
  // eligible for trimming.
  if (skip_trimming_due_to_suspend) {
    return;
  }
  if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE) {
    return;
  }

  // Try not to walk the graph too frequently because we can receive moderate
  // memory pressure notifications every 10s.

  if (!last_graph_walk_ || (base::TimeTicks::Now() - *last_graph_walk_ >
                            params_.graph_walk_backoff_time)) {
    TrimNodesOnGraph();
  }

  if (trim_arc_on_memory_pressure_) {
    if (!last_arc_process_fetch_ ||
        (base::TimeTicks::Now() - *last_arc_process_fetch_ >
         params_.arc_process_list_fetch_backoff_time)) {
      TrimArcProcesses();
    }
  }

  if (trim_arcvm_on_memory_pressure_) {
    if (!last_arcvm_trim_ || (base::TimeTicks::Now() - *last_arcvm_trim_ >
                              params_.arcvm_trim_backoff_time)) {
      TrimArcVmProcesses(level);
    }
  }
}

void WorkingSetTrimmerPolicyChromeOS::set_arcvm_delegate_for_testing(
    ArcVmDelegate* delegate) {
  DCHECK(!g_arcvm_delegate_for_testing || !delegate);
  g_arcvm_delegate_for_testing = delegate;
}

void WorkingSetTrimmerPolicyChromeOS::TrimNodesOnGraph() {
  const base::TimeTicks now_ticks = base::TimeTicks::Now();
  for (const PageNode* page_node : GetOwningGraph()->GetAllPageNodes()) {
    if (!page_node->IsVisible() &&
        page_node->GetTimeSinceLastVisibilityChange() >
            params_.node_invisible_time) {
      // Get the process node and if it has not been
      // trimmed within the backoff period, we will do that
      // now.

      // Check that we have a main frame.
      const FrameNode* frame_node = page_node->GetMainFrameNode();
      if (!frame_node) {
        continue;
      }

      const ProcessNode* process_node = frame_node->GetProcessNode();
      if (process_node && process_node->GetProcess().IsValid()) {
        base::TimeTicks last_trim = GetLastTrimTime(process_node);
        if (now_ticks - last_trim > params_.node_trim_backoff_time) {
          TrimWorkingSet(process_node);
        }
      }
    }
  }
  last_graph_walk_ = now_ticks;
}

base::TimeDelta WorkingSetTrimmerPolicyChromeOS::GetTimeSinceLastArcProcessTrim(
    base::ProcessId pid) const {
  base::TimeDelta delta(base::TimeDelta::Max());
  const auto it = arc_processes_last_trim_.find(pid);
  if (it != arc_processes_last_trim_.end()) {
    delta = base::TimeTicks::Now() - it->second;
  }
  return delta;
}

void WorkingSetTrimmerPolicyChromeOS::SetArcProcessLastTrimTime(
    base::ProcessId pid,
    base::TimeTicks time) {
  arc_processes_last_trim_[pid] = time;
}

bool WorkingSetTrimmerPolicyChromeOS::IsArcProcessEligibleForReclaim(
    const arc::ArcProcess& arc_process) {
  // Focused apps will never be reclaimed.
  if (arc_process.is_focused()) {
    return false;
  }

  if (!params_.trim_arc_aggressive) {
    // By default (non-aggressive) we will only trim unimportant apps
    // non-background protected apps.
    if (arc_process.IsImportant() || arc_process.IsBackgroundProtected()) {
      return false;
    }
  }

  // Next we need to check if it's been reclaimed too recently, if configured.
  if (params_.arc_process_trim_backoff_time != base::TimeDelta::Min()) {
    if (GetTimeSinceLastArcProcessTrim(arc_process.pid()) <
        params_.arc_process_trim_backoff_time) {
      return false;
    }
  }

  // Finally we check if the last activity time was longer than the configured
  // threshold, if configured.
  if (params_.arc_process_inactivity_time != base::TimeDelta::Min()) {
    // Are we within the threshold?
    if ((base::TimeTicks::Now() -
         base::TimeTicks::FromUptimeMillis(arc_process.last_activity_time())) <
        params_.arc_process_inactivity_time) {
      return false;
    }
  }

  return true;
}

mechanism::WorkingSetTrimmerChromeOS*
WorkingSetTrimmerPolicyChromeOS::GetTrimmer() {
  return static_cast<mechanism::WorkingSetTrimmerChromeOS*>(
      mechanism::WorkingSetTrimmer::GetInstance());
}

void WorkingSetTrimmerPolicyChromeOS::TrimArcProcess(base::ProcessId pid) {
  SetArcProcessLastTrimTime(pid, base::TimeTicks::Now());

  GetTrimmer()->TrimWorkingSet(pid);
}

void WorkingSetTrimmerPolicyChromeOS::TrimReceivedArcProcesses(
    int allowed_to_trim,
    arc::ArcProcessService::OptionalArcProcessList arc_processes) {
  if (!arc_processes.has_value()) {
    return;
  }

  // Because ARC may return the same list in order, we shuffle them each time in
  // case we have a small number of arc_available_processes_to_trim_ we don't
  // want to retrim the same ones every time.
  auto& procs = arc_processes.value();
  base::RandomShuffle(procs.begin(), procs.end());

  for (const auto& proc : procs) {
    // If we've already reclaimed too much, we bail.
    if (!allowed_to_trim) {
      break;
    }

    if (IsArcProcessEligibleForReclaim(proc)) {
      TrimArcProcess(proc.pid());

      if (allowed_to_trim > 0) {
        allowed_to_trim--;
      }
    }
  }
}

// TrimArcProcesses will be called on the PM Sequence, we'll need to bounce to
// the UI thread to get the Arc process list and we'll bounce back to the PM
// sequence to do the actual trimming and book keeping.
void WorkingSetTrimmerPolicyChromeOS::TrimArcProcesses() {
  last_arc_process_fetch_ = base::TimeTicks::Now();

  // The fetching of the ARC process list must happen on the UI thread.
  if (params_.trim_arc_system_processes) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&GetArcProcessListOnUIThread, ArcProcessType::kSystem,
                       weak_ptr_factory_.GetWeakPtr(),
                       params_.arc_max_number_processes_per_trim));
  }

  if (params_.trim_arc_app_processes) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&GetArcProcessListOnUIThread, ArcProcessType::kApp,
                       weak_ptr_factory_.GetWeakPtr(),
                       params_.arc_max_number_processes_per_trim));
  }
}

void WorkingSetTrimmerPolicyChromeOS::TrimArcVmProcesses(
    base::MemoryPressureListener::MemoryPressureLevel level) {
  DCHECK_NE(level, base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE);
  // TODO(crbug.com/40755583): Remove the PostTask once performance_manager code
  // is migrated to UI thread.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&TrimArcVmProcessesOnUIThread, level, params_,
                                weak_ptr_factory_.GetWeakPtr()));
}

// static
void WorkingSetTrimmerPolicyChromeOS::TrimArcVmProcessesOnUIThread(
    base::MemoryPressureListener::MemoryPressureLevel level,
    features::TrimOnMemoryPressureParams params,
    base::WeakPtr<WorkingSetTrimmerPolicyChromeOS> ptr) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // TODO(crbug.com/40755583): Let the policy own WorkingSetTrimmerPolicyArcVm
  // instance once performance_manager code is migrated to UI thread.
  auto* arcvm_delegate = g_arcvm_delegate_for_testing
                             ? g_arcvm_delegate_for_testing
                             : WorkingSetTrimmerPolicyArcVm::Get();

  const bool force_reclaim =
      params.trim_arcvm_on_critical_pressure &&
      (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL);
  const mechanism::ArcVmReclaimType trim_once_type_after_arcvm_boot =
      params.trim_arcvm_on_first_memory_pressure_after_arcvm_boot
          ? mechanism::ArcVmReclaimType::kReclaimGuestPageCaches
          : mechanism::ArcVmReclaimType::kReclaimNone;

  bool is_first_trim_post_boot =
      WorkingSetTrimmerPolicyArcVm::kNotFirstReclaimPostBoot;
  const mechanism::ArcVmReclaimType reclaim_type =
      force_reclaim
          ? mechanism::ArcVmReclaimType::kReclaimAll
          : arcvm_delegate->IsEligibleForReclaim(
                params.arcvm_inactivity_time, trim_once_type_after_arcvm_boot,
                &is_first_trim_post_boot);

  // NOTE: To ease unit test, we invoke OnTrimArcVmProcesses even
  // reclaim_type is kReclaimNone.
  PerformanceManager::CallOnGraph(
      FROM_HERE,
      base::BindOnce(&WorkingSetTrimmerPolicyChromeOS::OnTrimArcVmProcesses,
                     ptr, reclaim_type, is_first_trim_post_boot,
                     params.trim_arcvm_pages_per_minute,
                     params.trim_arcvm_max_pages_per_iteration));
}

void WorkingSetTrimmerPolicyChromeOS::OnTrimArcVmProcesses(
    mechanism::ArcVmReclaimType reclaim_type,
    bool is_first_trim_post_boot,
    int pages_per_minute,
    int max_pages_per_iteration) {
  // If there's nothing to do, cut it short.
  if (reclaim_type == mechanism::ArcVmReclaimType::kReclaimNone)
    return;

  // Computing the page limit requires touching the "this" pointer,
  // so it must be done in the PM thread.
  // Checking that "this" has not yet been deleted is done by BindOnce()
  // at invocation time.
  int page_limit = arc::ArcSession::kNoPageLimit;
  if (!is_first_trim_post_boot) {
    bool per_minute_limit_applied = false;
    if (pages_per_minute != arc::ArcSession::kNoPageLimit &&
        last_arcvm_trim_success_) {
      auto elapsed_mins =
          (base::TimeTicks::Now() - *last_arcvm_trim_success_).InMinutes();
      if (elapsed_mins > 0) {
        page_limit = elapsed_mins * pages_per_minute;
        per_minute_limit_applied = true;
      }  // else, let the per-iteration limit prevail.
    }

    if (max_pages_per_iteration != arc::ArcSession::kNoPageLimit) {
      // If set, the per-iteration max overrides the per-minute value.
      if (!per_minute_limit_applied || max_pages_per_iteration < page_limit)
        page_limit = max_pages_per_iteration;
    }
  }

  // TODO(crbug.com/40755583): Remove the PostTask once performance_manager code
  // is migrated to UI thread.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindRepeating(&DoTrimArcVmOnUIThread,
                                     weak_ptr_factory_.GetWeakPtr(),
                                     GetTrimmer(), reclaim_type, page_limit));
  if (reclaim_type == mechanism::ArcVmReclaimType::kReclaimAll)
    OnArcVmTrimStarting();
}

// static
void WorkingSetTrimmerPolicyChromeOS::DoTrimArcVmOnUIThread(
    base::WeakPtr<WorkingSetTrimmerPolicyChromeOS> ptr,
    mechanism::WorkingSetTrimmerChromeOS* trimmer,
    mechanism::ArcVmReclaimType reclaim_type,
    int page_limit) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  trimmer->TrimArcVmWorkingSet(
      base::BindOnce(&OnTrimArcVmWorkingSetOnUIThread, ptr, reclaim_type),
      reclaim_type, page_limit);
}

// static
void WorkingSetTrimmerPolicyChromeOS::OnTrimArcVmWorkingSetOnUIThread(
    base::WeakPtr<WorkingSetTrimmerPolicyChromeOS> ptr,
    mechanism::ArcVmReclaimType reclaim_type,
    bool success,
    const std::string& failure_reason) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // NOTE: To ease unit test, we invoke OnArcVmTrimEnded even when
  // |reclaim_type| is not kReclaimAll.
  PerformanceManager::CallOnGraph(
      FROM_HERE,
      base::BindOnce(&WorkingSetTrimmerPolicyChromeOS::OnArcVmTrimEnded, ptr,
                     reclaim_type, success));

  if (success) {
    VLOG(2) << "Reclaimed ARCVM memory";
    return;
  }
  LOG(WARNING) << "Failed to reclaim ARCVM memory: " << failure_reason;
}

void WorkingSetTrimmerPolicyChromeOS::OnArcVmTrimStarting() {
  last_arcvm_trim_ = base::TimeTicks::Now();
}

void WorkingSetTrimmerPolicyChromeOS::OnArcVmTrimEnded(
    mechanism::ArcVmReclaimType reclaim_type,
    bool success) {
  if (reclaim_type != mechanism::ArcVmReclaimType::kReclaimAll)
    return;
  if (success)
    last_arcvm_trim_success_ = base::TimeTicks::Now();
}

void WorkingSetTrimmerPolicyChromeOS::OnTakenFromGraph(Graph* graph) {
  memory_pressure_listener_.reset();
  WorkingSetTrimmerPolicy::OnTakenFromGraph(graph);
}

void WorkingSetTrimmerPolicyChromeOS::OnAllFramesInProcessFrozen(
    const ProcessNode* process_node) {
  if (trim_on_freeze_) {
    WorkingSetTrimmerPolicy::OnAllFramesInProcessFrozen(process_node);
  }
}

void WorkingSetTrimmerPolicyChromeOS::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  base::AutoLock lock(mutex_);
  is_system_suspended_ = true;
}

void WorkingSetTrimmerPolicyChromeOS::SuspendDone(base::TimeDelta duration) {
  base::TimeTicks now = base::TimeTicks::Now();
  base::AutoLock lock(mutex_);
  is_system_suspended_ = false;
  last_suspend_done_time_ = now;
}

void WorkingSetTrimmerPolicyChromeOS::OnPassedToGraph(Graph* graph) {
  // We wait to register the memory pressure listener so we're on the
  // right sequence.
  params_ = features::TrimOnMemoryPressureParams::GetParams();
  memory_pressure_listener_.emplace(
      FROM_HERE,
      base::BindRepeating(&WorkingSetTrimmerPolicyChromeOS::OnMemoryPressure,
                          base::Unretained(this)));

  WorkingSetTrimmerPolicy::OnPassedToGraph(graph);
}

// static
bool WorkingSetTrimmerPolicyChromeOS::PlatformSupportsWorkingSetTrim() {
  return mechanism::WorkingSetTrimmer::GetInstance()
      ->PlatformSupportsWorkingSetTrim();
}

}  // namespace policies
}  // namespace performance_manager