chromium/chrome/browser/performance_manager/policies/working_set_trimmer_policy_arcvm.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/policies/working_set_trimmer_policy_arcvm.h"

#include "ash/components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "ash/components/arc/arc_util.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/public/cpp/app_types_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/singleton.h"
#include "base/no_destructor.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/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/exo/wm_helper.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"

namespace performance_manager {
namespace policies {
namespace {

// The delay until |is_boot_complete_and_connected_| is flipped after app.mojom
// and intent_helper.mojom are connected, The delay is necessary because when
// the connections are established, ARCVM is often still busy accessing many
// files. The current value (100s) is based on the slowest SKU (Bluebird) of the
// Octopus family. It usually takes about 50 seconds for the device to become
// idle after the mojo connections ready event, and the 100s setting is 50s * 2
// (safety factor) considering that we might launch ARCVM on (slightly) slower
// devices than Octopus in the future.
constexpr base::TimeDelta kArcVmBootDelay = base::Seconds(100);

content::BrowserContext* GetContext() {
  // For production, always use the primary user profile. ARCVM does not
  // support non-primary profiles. |g_browser_process| can be nullptr during
  // browser shutdown.
  if (g_browser_process && g_browser_process->profile_manager())
    return g_browser_process->profile_manager()->GetPrimaryUserProfile();
  return nullptr;
}

}  // namespace

// static
std::unique_ptr<WorkingSetTrimmerPolicyArcVm>
WorkingSetTrimmerPolicyArcVm::CreateForTesting(
    content::BrowserContext* context) {
  auto* policy = new WorkingSetTrimmerPolicyArcVm();
  policy->context_for_testing_ = context;
  return base::WrapUnique(policy);
}

// static
WorkingSetTrimmerPolicyArcVm* WorkingSetTrimmerPolicyArcVm::Get() {
  static base::NoDestructor<WorkingSetTrimmerPolicyArcVm> arcvm_policy;
  return arcvm_policy.get();
}

WorkingSetTrimmerPolicyArcVm::WorkingSetTrimmerPolicyArcVm() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(arc::IsArcVmEnabled()) << "This is only for ARCVM builds";

  auto* arc_session_manager = arc::ArcSessionManager::Get();
  // ArcSessionManager is created very early in
  // `ChromeBrowserMainPartsAsh::PreMainMessageLoopRun()`.
  DCHECK(arc_session_manager);
  arc_session_manager->AddObserver(this);

  if (exo::WMHelper::HasInstance()) {  // for unit tests
    auto* helper = exo::WMHelper::GetInstance();
    helper->AddActivationObserver(this);
    OnWindowActivated(
        wm::ActivationChangeObserver::ActivationReason::ACTIVATION_CLIENT,
        helper->GetActiveWindow(), /*lost_active=*/nullptr);
  }

  // If app() and/or intent_helper() are already connected to the instance in
  // the guest, the OnConnectionReady() function is synchronously called before
  // returning from AddObserver. For more details, see
  // ash/components/arc/session/connection_holder.h, especially its
  // AddObserver() function.
  auto* arc_service_manager = arc::ArcServiceManager::Get();
  // ArcServiceManager and objects owned by the manager are created very early
  // in `ChromeBrowserMainPartsAsh::PreMainMessageLoopRun()` too.
  DCHECK(arc_service_manager);
  arc_service_manager->arc_bridge_service()->app()->AddObserver(this);
  arc_service_manager->arc_bridge_service()->intent_helper()->AddObserver(this);
}

WorkingSetTrimmerPolicyArcVm::~WorkingSetTrimmerPolicyArcVm() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  content::BrowserContext* context =
      context_for_testing_ ? context_for_testing_.get() : GetContext();
  if (context) {
    auto* metrics_service =
        arc::ArcMetricsService::GetForBrowserContext(context);
    if (metrics_service)
      metrics_service->RemoveUserInteractionObserver(this);
  }

  auto* arc_service_manager = arc::ArcServiceManager::Get();
  arc_service_manager->arc_bridge_service()->intent_helper()->RemoveObserver(
      this);
  arc_service_manager->arc_bridge_service()->app()->RemoveObserver(this);

  if (exo::WMHelper::HasInstance())
    exo::WMHelper::GetInstance()->RemoveActivationObserver(this);

  auto* arc_session_manager = arc::ArcSessionManager::Get();
  if (arc_session_manager)
    arc_session_manager->RemoveObserver(this);
}

mechanism::ArcVmReclaimType WorkingSetTrimmerPolicyArcVm::IsEligibleForReclaim(
    const base::TimeDelta& arcvm_inactivity_time,
    mechanism::ArcVmReclaimType trim_once_type_after_arcvm_boot,
    bool* is_first_trim_post_boot) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (is_first_trim_post_boot)
    *is_first_trim_post_boot = false;  // Unless proven otherwise, below.
  if (!is_boot_complete_and_connected_)
    return mechanism::ArcVmReclaimType::kReclaimNone;
  if (!trimmed_at_boot_ && trim_once_type_after_arcvm_boot !=
                               mechanism::ArcVmReclaimType::kReclaimNone) {
    if (is_first_trim_post_boot)
      *is_first_trim_post_boot = true;
    trimmed_at_boot_ = true;
    return trim_once_type_after_arcvm_boot;
  }
  const bool is_inactive =
      (base::TimeTicks::Now() - last_user_interaction_) > arcvm_inactivity_time;
  return (!is_focused_ && is_inactive)
             ? mechanism::ArcVmReclaimType::kReclaimAll
             : mechanism::ArcVmReclaimType::kReclaimNone;
}

void WorkingSetTrimmerPolicyArcVm::OnUserInteraction(
    arc::UserInteractionType type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  last_user_interaction_ = base::TimeTicks::Now();
}

void WorkingSetTrimmerPolicyArcVm::OnArcSessionStopped(
    arc::ArcStopReason stop_reason) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // OnConnectionReadyInternal() shouldn't be called anymore.
  timer_.Stop();

  is_boot_complete_and_connected_ = false;
  trimmed_at_boot_ = false;
}

void WorkingSetTrimmerPolicyArcVm::OnArcSessionRestarting() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // OnConnectionReadyInternal() shouldn't be called anymore.
  timer_.Stop();

  is_boot_complete_and_connected_ = false;
  trimmed_at_boot_ = false;
}

void WorkingSetTrimmerPolicyArcVm::OnConnectionReady() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Check if both mojo connections are established.
  auto* arc_service_manager = arc::ArcServiceManager::Get();
  const bool app_connected =
      arc_service_manager &&
      arc_service_manager->arc_bridge_service()->app()->IsConnected();
  const bool intent_helper_connected =
      arc_service_manager &&
      arc_service_manager->arc_bridge_service()->intent_helper()->IsConnected();
  if (!app_connected || !intent_helper_connected)
    return;

  // Wait for a while more until ARCVM stops accessing too many files. Using
  // Unretained() is safe here because |this| object is created with
  // base::NoDestructor<> in the factory method above.
  timer_.Start(
      FROM_HERE, kArcVmBootDelay,
      base::BindOnce(&WorkingSetTrimmerPolicyArcVm::OnConnectionReadyInternal,
                     base::Unretained(this)));
}

void WorkingSetTrimmerPolicyArcVm::OnConnectionReadyInternal() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  is_boot_complete_and_connected_ = true;
  // Now the user is able to interact with ARCVM. Reset the value.
  last_user_interaction_ = base::TimeTicks::Now();
  if (!observing_user_interactions_) {
    StartObservingUserInteractions();
    observing_user_interactions_ = true;
  }
}

void WorkingSetTrimmerPolicyArcVm::OnWindowActivated(
    wm::ActivationChangeObserver::ActivationReason reason,
    aura::Window* gained_active,
    aura::Window* lost_active) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  bool was_focused = is_focused_;
  is_focused_ = ash::IsArcWindow(gained_active);
  if (was_focused && !is_focused_) {
    // While the window was focused, the user was interacting with ARCVM.
    last_user_interaction_ = base::TimeTicks::Now();
  }
}

// static
const base::TimeDelta&
WorkingSetTrimmerPolicyArcVm::GetArcVmBootDelayForTesting() {
  return kArcVmBootDelay;
}

void WorkingSetTrimmerPolicyArcVm::StartObservingUserInteractions() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  content::BrowserContext* context =
      context_for_testing_ ? context_for_testing_.get() : GetContext();
  DCHECK(context);

  // ArcMetricsService is created in OnPrimaryUserProfilePrepared() in
  // ArcServiceLauncher which also initializes objects that are needed to start
  // ARCVM e.g. ArcSessionManager. As long as the function is called after ARCVM
  // is started, e.g. from OnConnectionReady(), the DCHECK below should never
  // fail.
  auto* metrics_service = arc::ArcMetricsService::GetForBrowserContext(context);
  DCHECK(metrics_service);
  DCHECK(!observing_user_interactions_);
  metrics_service->AddUserInteractionObserver(this);
}

}  // namespace policies
}  // namespace performance_manager