chromium/chromeos/ash/components/dbus/resourced/resourced_client.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 "chromeos/ash/components/dbus/resourced/resourced_client.h"

#include "base/check_op.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/process/process_metrics.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/resource_manager/resource_manager.pb.h"
#include "chromeos/ash/components/dbus/resourced/fake_resourced_client.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "third_party/cros_system_api/dbus/resource_manager/dbus-constants.h"

namespace ash {
namespace {

// Resource manager D-Bus method calls are all simple operations and should
// not take more than 1 second.
constexpr int kResourcedDBusTimeoutMilliseconds = 1000;

ResourcedClient* g_instance = nullptr;

class ResourcedClientImpl : public ResourcedClient {
 public:
  ResourcedClientImpl();
  ~ResourcedClientImpl() override = default;
  ResourcedClientImpl(const ResourcedClientImpl&) = delete;
  ResourcedClientImpl& operator=(const ResourcedClientImpl&) = delete;

  void Init(dbus::Bus* bus) {
    proxy_ = bus->GetObjectProxy(
        resource_manager::kResourceManagerServiceName,
        dbus::ObjectPath(resource_manager::kResourceManagerServicePath));
    proxy_->ConnectToSignal(
        resource_manager::kResourceManagerInterface,
        resource_manager::kMemoryPressureChrome,
        base::BindRepeating(&ResourcedClientImpl::MemoryPressureReceived,
                            weak_factory_.GetWeakPtr()),
        base::BindOnce(&ResourcedClientImpl::MemoryPressureConnected,
                       weak_factory_.GetWeakPtr()));
    proxy_->ConnectToSignal(
        resource_manager::kResourceManagerInterface,
        resource_manager::kMemoryPressureArcContainer,
        base::BindRepeating(
            &ResourcedClientImpl::MemoryPressureArcContainerReceived,
            weak_factory_.GetWeakPtr()),
        base::BindOnce(&ResourcedClientImpl::MemoryPressureConnected,
                       weak_factory_.GetWeakPtr()));
  }

  // ResourcedClient interface.
  void SetGameModeWithTimeout(
      GameMode game_mode,
      uint32_t refresh_seconds,
      chromeos::DBusMethodCallback<GameMode> callback) override;

  void SetMemoryMarginsBps(uint32_t critical_margin,
                           uint32_t moderate_margin,
                           SetMemoryMarginsBpsCallback callback) override;

  void ReportBrowserProcesses(Component component,
                              const std::vector<Process>& processes) override;

  void SetProcessState(base::ProcessId process_id,
                       resource_manager::ProcessState state,
                       SetQoSStateCallback callback) override;

  void SetThreadState(base::ProcessId process_id,
                      base::PlatformThreadId thread_id,
                      resource_manager::ThreadState state,
                      SetQoSStateCallback callback) override;

  void AddObserver(Observer* observer) override;

  void RemoveObserver(Observer* observer) override;

  void AddArcContainerObserver(ArcContainerObserver* observer) override;

  void RemoveArcContainerObserver(ArcContainerObserver* observer) override;

  void WaitForServiceToBeAvailable(
      dbus::ObjectProxy::WaitForServiceToBeAvailableCallback callback) override;

 private:
  // D-Bus response handlers.
  void HandleSetGameModeWithTimeoutResponse(
      chromeos::DBusMethodCallback<GameMode> callback,
      dbus::Response* response);

  void HandleSetMemoryMarginBps(uint32_t critical_margin,
                                uint32_t moderate_margin,
                                SetMemoryMarginsBpsCallback callback,
                                dbus::Response* response);

  // D-Bus signal handlers.
  void MemoryPressureReceived(dbus::Signal* signal);
  void MemoryPressureConnected(const std::string& interface_name,
                               const std::string& signal_name,
                               bool success);

  void MemoryPressureArcContainerReceived(dbus::Signal* signal);

  void HandleSetProcessStateResponse(base::ProcessId process_id,
                                     SetQoSStateCallback callback,
                                     dbus::Response* response,
                                     dbus::ErrorResponse* error);

  void HandleSetThreadStateResponse(base::PlatformThreadId thread_id,
                                    SetQoSStateCallback callback,
                                    dbus::Response* response,
                                    dbus::ErrorResponse* error);

  // Member variables.

  raw_ptr<dbus::ObjectProxy> proxy_ = nullptr;

  // Caches the total memory for reclaim_target_kb sanity check. The default
  // value is 32 GiB in case reading total memory failed.
  uint64_t total_memory_kb_ = 32 * 1024 * 1024;

  // A list of observers that are listening on state changes, etc.
  base::ObserverList<Observer> observers_;

  // A list of observers listening for ARC container memory pressure signals.
  base::ObserverList<ArcContainerObserver> arc_container_observers_;

  base::WeakPtrFactory<ResourcedClientImpl> weak_factory_{this};
};

ResourcedClientImpl::ResourcedClientImpl() {
  base::SystemMemoryInfoKB info;
  if (base::GetSystemMemoryInfo(&info)) {
    total_memory_kb_ = static_cast<uint64_t>(info.total);
  } else {
    PLOG(ERROR) << "Error reading total memory.";
  }
}

void ResourcedClientImpl::MemoryPressureReceived(dbus::Signal* signal) {
  dbus::MessageReader signal_reader(signal);

  memory_pressure::ReclaimTarget reclaim_target;
  uint8_t pressure_level_byte;
  PressureLevel pressure_level;

  if (!signal_reader.PopByte(&pressure_level_byte) ||
      !signal_reader.PopUint64(&reclaim_target.target_kb)) {
    LOG(ERROR) << "Error reading signal from resourced: " << signal->ToString();
    return;
  }

  int64_t signal_origin_timestamp_ms = -1;
  // The signal origin timestamp may not be included by resourced, and if it is,
  // it might be an invalid value.
  if (signal_reader.PopInt64(&signal_origin_timestamp_ms) &&
      signal_origin_timestamp_ms > 0) {
    // Signal origin timestamp is received as a ms value from CLOCK_MONOTONIC.
    reclaim_target.origin_time =
        base::TimeTicks::FromUptimeMillis(signal_origin_timestamp_ms);
  }

  if (pressure_level_byte == resource_manager::PressureLevelChrome::NONE) {
    pressure_level = PressureLevel::NONE;
  } else if (pressure_level_byte ==
             resource_manager::PressureLevelChrome::MODERATE) {
    pressure_level = PressureLevel::MODERATE;
  } else if (pressure_level_byte ==
             resource_manager::PressureLevelChrome::CRITICAL) {
    pressure_level = PressureLevel::CRITICAL;
  } else {
    LOG(ERROR) << "Unknown memory pressure level: " << pressure_level_byte;
    return;
  }

  if (reclaim_target.target_kb > total_memory_kb_) {
    LOG(ERROR) << "reclaim_target_kb is too large: "
               << reclaim_target.target_kb;
    return;
  }

  for (auto& observer : observers_) {
    observer.OnMemoryPressure(pressure_level, reclaim_target);
  }
}

void ResourcedClientImpl::MemoryPressureArcContainerReceived(
    dbus::Signal* signal) {
  dbus::MessageReader signal_reader(signal);

  uint8_t pressure_level_byte;
  PressureLevelArcContainer pressure_level;
  uint64_t reclaim_target_kb;

  if (!signal_reader.PopByte(&pressure_level_byte) ||
      !signal_reader.PopUint64(&reclaim_target_kb)) {
    LOG(ERROR) << "Error reading signal from resourced: " << signal->ToString();
    return;
  }
  switch (static_cast<resource_manager::PressureLevelArcContainer>(
      pressure_level_byte)) {
    case resource_manager::PressureLevelArcContainer::NONE:
      pressure_level = PressureLevelArcContainer::kNone;
      break;

    case resource_manager::PressureLevelArcContainer::CACHED:
      pressure_level = PressureLevelArcContainer::kCached;
      break;

    case resource_manager::PressureLevelArcContainer::PERCEPTIBLE:
      pressure_level = PressureLevelArcContainer::kPerceptible;
      break;

    case resource_manager::PressureLevelArcContainer::FOREGROUND:
      pressure_level = PressureLevelArcContainer::kForeground;
      break;

    default:
      LOG(ERROR) << "Unknown memory pressure level: " << pressure_level_byte;
      return;
  }

  if (reclaim_target_kb > total_memory_kb_) {
    LOG(ERROR) << "reclaim_target_kb is too large: " << reclaim_target_kb;
    return;
  }

  for (auto& observer : arc_container_observers_) {
    observer.OnMemoryPressure(pressure_level, reclaim_target_kb);
  }
}

void ResourcedClientImpl::HandleSetProcessStateResponse(
    base::ProcessId process_id,
    SetQoSStateCallback callback,
    dbus::Response* response,
    dbus::ErrorResponse* error) {
  dbus::DBusResult result = dbus::DBusResult::kSuccess;
  if (response == nullptr) {
    result = dbus::GetResult(error);
  }
  std::move(callback).Run(result);
}

void ResourcedClientImpl::HandleSetThreadStateResponse(
    base::PlatformThreadId thread_id,
    SetQoSStateCallback callback,
    dbus::Response* response,
    dbus::ErrorResponse* error) {
  dbus::DBusResult result = dbus::DBusResult::kSuccess;
  if (response == nullptr) {
    result = dbus::GetResult(error);
  }
  std::move(callback).Run(result);
}

void ResourcedClientImpl::MemoryPressureConnected(
    const std::string& interface_name,
    const std::string& signal_name,
    bool success) {
  PLOG_IF(ERROR, !success) << "Failed to connect to signal: " << signal_name;
}

// Response will be true if game mode was on previously, false otherwise.
void ResourcedClientImpl::HandleSetGameModeWithTimeoutResponse(
    chromeos::DBusMethodCallback<GameMode> callback,
    dbus::Response* response) {
  dbus::MessageReader reader(response);
  uint8_t previous;
  if (!reader.PopByte(&previous)) {
    std::move(callback).Run(std::nullopt);
    return;
  }
  std::move(callback).Run(static_cast<GameMode>(previous));
}

void ResourcedClientImpl::SetGameModeWithTimeout(
    GameMode game_mode,
    uint32_t refresh_seconds,
    chromeos::DBusMethodCallback<GameMode> callback) {
  dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
                               resource_manager::kSetGameModeWithTimeoutMethod);
  dbus::MessageWriter writer(&method_call);
  writer.AppendByte(static_cast<uint8_t>(game_mode));
  writer.AppendUint32(refresh_seconds);

  proxy_->CallMethod(
      &method_call, kResourcedDBusTimeoutMilliseconds,
      base::BindOnce(&ResourcedClientImpl::HandleSetGameModeWithTimeoutResponse,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void ResourcedClientImpl::HandleSetMemoryMarginBps(
    uint32_t critical_margin,
    uint32_t moderate_margin,
    SetMemoryMarginsBpsCallback callback,
    dbus::Response* response) {
  if (callback.is_null()) {
    return;
  }

  if (!response) {
    LOG(ERROR) << "Null response object received: try again in 30 seconds.";

    // If Chrome startup was racing with resourced startup it's possible
    // that the message was not delivered because resourced was not up yet.
    // Let's redispatch the message in 30 seconds.
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&ResourcedClientImpl::SetMemoryMarginsBps,
                       weak_factory_.GetWeakPtr(), critical_margin,
                       moderate_margin, std::move(callback)),
        base::Seconds(30));
    return;
  }

  uint64_t critical = 0;
  uint64_t moderate = 0;
  dbus::MessageReader reader(response);
  if (!reader.PopUint64(&critical) || !reader.PopUint64(&moderate)) {
    LOG(ERROR) << "Unable to read back uint64s from resourced";
    std::move(callback).Run(false, 0, 0);
    return;
  }

  std::move(callback).Run(true, critical, moderate);
}

void ResourcedClientImpl::SetMemoryMarginsBps(
    uint32_t critical_margin,
    uint32_t moderate_margin,
    SetMemoryMarginsBpsCallback callback) {
  dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
                               resource_manager::kSetMemoryMarginsBps);
  dbus::MessageWriter writer(&method_call);
  writer.AppendUint32(critical_margin);
  writer.AppendUint32(moderate_margin);

  proxy_->CallMethod(
      &method_call, kResourcedDBusTimeoutMilliseconds,
      base::BindOnce(&ResourcedClientImpl::HandleSetMemoryMarginBps,
                     weak_factory_.GetWeakPtr(), critical_margin,
                     moderate_margin, std::move(callback)));
}

void ResourcedClientImpl::ReportBrowserProcesses(
    Component component,
    const std::vector<Process>& processes) {
  resource_manager::ReportBrowserProcesses request;

  if (component == ResourcedClient::Component::kAsh) {
    request.set_browser_type(resource_manager::BrowserType::ASH);
  } else if (component == ResourcedClient::Component::kLacros) {
    request.set_browser_type(resource_manager::BrowserType::LACROS);
  } else {
    NOTREACHED_IN_MIGRATION();
  }

  for (auto it = processes.begin(); it != processes.end(); ++it) {
    auto* process = request.add_processes();
    process->set_pid(it->pid);
    process->set_protected_(it->is_protected);
    process->set_visible(it->is_visible);
    process->set_focused(it->is_focused);
    process->set_last_visible_ms(
        it->last_visible.since_origin().InMilliseconds());
  }

  dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
                               resource_manager::kReportBrowserProcessesMethod);
  if (!dbus::MessageWriter(&method_call).AppendProtoAsArrayOfBytes(request)) {
    LOG(ERROR) << "Error serializing "
               << resource_manager::kReportBrowserProcessesMethod << " request";
    return;
  }

  proxy_->CallMethod(&method_call, kResourcedDBusTimeoutMilliseconds,
                     base::DoNothing());
}

void ResourcedClientImpl::SetProcessState(base::ProcessId process_id,
                                          resource_manager::ProcessState state,
                                          SetQoSStateCallback callback) {
  dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
                               resource_manager::kSetProcessStateMethod);
  dbus::MessageWriter writer(&method_call);

  writer.AppendUint32(process_id);
  writer.AppendByte(static_cast<uint8_t>(state));

  proxy_->CallMethodWithErrorResponse(
      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&ResourcedClientImpl::HandleSetProcessStateResponse,
                     weak_factory_.GetWeakPtr(), process_id,
                     std::move(callback)));
}

void ResourcedClientImpl::SetThreadState(base::ProcessId process_id,
                                         base::PlatformThreadId thread_id,
                                         resource_manager::ThreadState state,
                                         SetQoSStateCallback callback) {
  dbus::MethodCall method_call(resource_manager::kResourceManagerInterface,
                               resource_manager::kSetThreadStateMethod);
  dbus::MessageWriter writer(&method_call);

  writer.AppendUint32(process_id);
  writer.AppendUint32(thread_id);
  writer.AppendByte(static_cast<uint8_t>(state));

  proxy_->CallMethodWithErrorResponse(
      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
      base::BindOnce(&ResourcedClientImpl::HandleSetThreadStateResponse,
                     weak_factory_.GetWeakPtr(), thread_id,
                     std::move(callback)));
}

void ResourcedClientImpl::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ResourcedClientImpl::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void ResourcedClientImpl::AddArcContainerObserver(
    ArcContainerObserver* observer) {
  arc_container_observers_.AddObserver(observer);
}

void ResourcedClientImpl::RemoveArcContainerObserver(
    ArcContainerObserver* observer) {
  arc_container_observers_.RemoveObserver(observer);
}

void ResourcedClientImpl::WaitForServiceToBeAvailable(
    dbus::ObjectProxy::WaitForServiceToBeAvailableCallback callback) {
  proxy_->WaitForServiceToBeAvailable(std::move(callback));
}

}  // namespace

ResourcedClient::ResourcedClient() {
  CHECK(!g_instance);
  g_instance = this;
}

ResourcedClient::~ResourcedClient() {
  CHECK_EQ(this, g_instance);
  g_instance = nullptr;
}

// static
void ResourcedClient::Initialize(dbus::Bus* bus) {
  CHECK(bus);
  (new ResourcedClientImpl())->Init(bus);
}

// static
FakeResourcedClient* ResourcedClient::InitializeFake() {
  return new FakeResourcedClient();
}

// static
void ResourcedClient::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
  // The destructor resets |g_instance|.
  DCHECK(!g_instance);
}

// static
ResourcedClient* ResourcedClient::Get() {
  return g_instance;
}

}  // namespace ash