chromium/chrome/browser/android/oom_intervention/oom_intervention_decider.cc

// Copyright 2017 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/android/oom_intervention/oom_intervention_decider.h"

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "components/metrics/metrics_service.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/scoped_user_pref_update.h"

namespace {

const char kOomInterventionDecider[] = "oom_intervention.decider";

// Deprecated: Replaced with `kBlocklist`.
// TODO(crbug.com/40744119): Remove this after M92 once existing
// clients have migrated to the new pref.
const char kBlacklist[] = "oom_intervention.blacklist";
// Pref path for blocklist. If a hostname is in the blocklist we never trigger
// intervention on the host.
const char kBlocklist[] = "oom_intervention.blocklist";
// Pref path for declined host list. If a hostname is in the declined host list
// we don't trigger intervention until a OOM crash happens on the host.
const char kDeclinedHostList[] = "oom_intervention.declined_host_list";
// Pref path for OOM detected host list. When an OOM crash is observed on
// a host the hostname is added to the list.
const char kOomDetectedHostList[] = "oom_intervention.oom_detected_host_list";

class DelegateImpl : public OomInterventionDecider::Delegate {
 public:
  bool WasLastShutdownClean() override {
    if (!g_browser_process || !g_browser_process->metrics_service())
      return true;
    return g_browser_process->metrics_service()->WasLastShutdownClean();
  }
};

}  // namespace

const size_t OomInterventionDecider::kMaxListSize = 10;
const size_t OomInterventionDecider::kMaxBlocklistSize = 6;

// static
void OomInterventionDecider::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterListPref(kBlocklist);
  registry->RegisterListPref(kDeclinedHostList);
  registry->RegisterListPref(kOomDetectedHostList);

  // Continue to register the old preference to migrate its value.
  registry->RegisterListPref(kBlacklist);
}

// static
OomInterventionDecider* OomInterventionDecider::GetForBrowserContext(
    content::BrowserContext* context) {
  // The OomIntervetnionDecider is disabled in incognito mode because it is
  // written in such a way that hostnames would be persisted in preferences on
  // disk which is not acceptable for incognito mode.
  if (context->IsOffTheRecord())
    return nullptr;

  if (!context->GetUserData(kOomInterventionDecider)) {
    PrefService* prefs = Profile::FromBrowserContext(context)->GetPrefs();
    context->SetUserData(kOomInterventionDecider,
                         base::WrapUnique(new OomInterventionDecider(
                             std::make_unique<DelegateImpl>(), prefs)));
  }
  return static_cast<OomInterventionDecider*>(
      context->GetUserData(kOomInterventionDecider));
}

OomInterventionDecider::OomInterventionDecider(
    std::unique_ptr<Delegate> delegate,
    PrefService* prefs)
    : delegate_(std::move(delegate)), prefs_(prefs) {
  DCHECK(delegate_);

  PrefService::PrefInitializationStatus pref_status =
      prefs_->GetInitializationStatus();
  if (pref_status == PrefService::INITIALIZATION_STATUS_WAITING) {
    prefs_->AddPrefInitObserver(base::BindOnce(
        &OomInterventionDecider::OnPrefInitialized, base::Unretained(this)));
  } else {
    OnPrefInitialized(pref_status ==
                      PrefService::INITIALIZATION_STATUS_SUCCESS);
  }
}

OomInterventionDecider::~OomInterventionDecider() = default;

bool OomInterventionDecider::CanTriggerIntervention(
    const std::string& host) const {
  if (IsOptedOut(host))
    return false;

  // Check whether OOM was observed before checking declined host list in favor
  // of triggering intervention after OOM.
  if (IsInList(kOomDetectedHostList, host))
    return true;

  if (IsInList(kDeclinedHostList, host))
    return false;

  return true;
}

void OomInterventionDecider::OnInterventionDeclined(const std::string& host) {
  if (IsOptedOut(host))
    return;

  if (IsInList(kDeclinedHostList, host)) {
    AddToList(kBlocklist, host);
  } else {
    AddToList(kDeclinedHostList, host);
  }
}

void OomInterventionDecider::OnOomDetected(const std::string& host) {
  if (IsOptedOut(host))
    return;
  AddToList(kOomDetectedHostList, host);
}

void OomInterventionDecider::ClearData() {
  prefs_->ClearPref(kBlocklist);
  prefs_->ClearPref(kDeclinedHostList);
  prefs_->ClearPref(kOomDetectedHostList);
}

void OomInterventionDecider::OnPrefInitialized(bool success) {
  if (!success)
    return;

  // Migrate `kBlacklist` to `kBlocklist`.
  const base::Value::List& old_pref_value = prefs_->GetList(kBlacklist);
  if (!old_pref_value.empty()) {
    prefs_->SetList(kBlocklist, old_pref_value.Clone());
    prefs_->SetList(kBlacklist, base::Value::List());
  }

  if (delegate_->WasLastShutdownClean())
    return;

  const base::Value::List& declined_list = prefs_->GetList(kDeclinedHostList);
  if (!declined_list.empty()) {
    const std::string& last_declined = declined_list.back().GetString();
    if (!IsInList(kBlocklist, last_declined))
      AddToList(kOomDetectedHostList, last_declined);
  }
}

bool OomInterventionDecider::IsOptedOut(const std::string& host) const {
  if (prefs_->GetList(kBlocklist).size() >= kMaxBlocklistSize)
    return true;

  return IsInList(kBlocklist, host);
}

bool OomInterventionDecider::IsInList(const char* list_name,
                                      const std::string& host) const {
  for (const auto& value : prefs_->GetList(list_name)) {
    if (value.GetString() == host)
      return true;
  }
  return false;
}

void OomInterventionDecider::AddToList(const char* list_name,
                                       const std::string& host) {
  if (IsInList(list_name, host))
    return;
  ScopedListPrefUpdate update(prefs_, list_name);
  base::Value::List& update_list = update.Get();
  update_list.Append(host);
  if (update_list.size() > kMaxListSize)
    update_list.erase(update_list.begin());

  // Save the list immediately because we typically modify lists under high
  // memory pressure, in which the browser process can be killed by the OS
  // soon.
  prefs_->CommitPendingWrite();
}