chromium/chrome/browser/ash/schedqos/dbus_schedqos_state_handler.cc

// Copyright 2024 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/ash/schedqos/dbus_schedqos_state_handler.h"

#include <utility>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/synchronization/lock.h"
#include "base/timer/elapsed_timer.h"
#include "chrome/browser/ash/system/procfs_util.h"
#include "chromeos/ash/components/dbus/resourced/resourced_client.h"
#include "dbus/dbus_result.h"
#include "third_party/cros_system_api/dbus/resource_manager/dbus-constants.h"

namespace ash {

namespace {

DBusSchedQOSStateHandler::PidReuseResult GetPidReuseResult(bool is_pid_reused,
                                                           bool success) {
  if (success) {
    return is_pid_reused
               ? DBusSchedQOSStateHandler::PidReuseResult::kPidReuseOnSuccess
               : DBusSchedQOSStateHandler::PidReuseResult::
                     kNotPidReuseOnSuccess;
  }
  return is_pid_reused
             ? DBusSchedQOSStateHandler::PidReuseResult::kPidReuseOnFail
             : DBusSchedQOSStateHandler::PidReuseResult::kNotPidReuseOnFail;
}

bool IsPidReused(system::ProcStatFile stat_file,
                 base::ProcessId pid,
                 bool success) {
  if (success && !stat_file.IsValid()) {
    // If the stat file does not exist before sending the request but the
    // request succeeds, it means the process/thread is dead and it applies
    // request to another process/thread with the same pid.
    return true;
  }

  return (!stat_file.IsPidAlive() && system::ProcStatFile(pid).IsValid());
}
}  // namespace

DBusSchedQOSStateHandler::ProcessState::ProcessState(
    base::Process::Priority priority)
    : ProcessState(priority, false) {}

DBusSchedQOSStateHandler::ProcessState::ProcessState(
    base::Process::Priority priority,
    bool need_retry)
    : priority(priority), need_retry(need_retry) {}

DBusSchedQOSStateHandler::ProcessState::~ProcessState() = default;
DBusSchedQOSStateHandler::ProcessState::ProcessState(ProcessState&&) = default;

DBusSchedQOSStateHandler::DBusSchedQOSStateHandler(
    scoped_refptr<base::SequencedTaskRunner> main_task_runner)
    : main_task_runner_(main_task_runner) {
  base::Process::SetProcessPriorityDelegate(this);
  base::PlatformThread::SetThreadTypeDelegate(this);
  base::PlatformThread::SetCrossProcessPlatformThreadDelegate(this);
}

DBusSchedQOSStateHandler::~DBusSchedQOSStateHandler() {
  base::PlatformThread::SetCrossProcessPlatformThreadDelegate(nullptr);
  base::PlatformThread::SetThreadTypeDelegate(nullptr);
  base::Process::SetProcessPriorityDelegate(nullptr);
}

// static
DBusSchedQOSStateHandler* DBusSchedQOSStateHandler::Create(
    scoped_refptr<base::SequencedTaskRunner> main_task_runner) {
  auto* handler = new DBusSchedQOSStateHandler(main_task_runner);

  ash::ResourcedClient::Get()->WaitForServiceToBeAvailable(
      base::BindOnce(&DBusSchedQOSStateHandler::OnServiceConnected,
                     handler->weak_ptr_factory_.GetWeakPtr()));

  return handler;
}

bool DBusSchedQOSStateHandler::CanSetProcessPriority() {
  return true;
}

void DBusSchedQOSStateHandler::InitializeProcessPriority(
    base::ProcessId process_id) {
  // Processes in ChromeOS are Priority::kUserBlocking by default.
  base::Process::Priority default_priority =
      base::Process::Priority::kUserBlocking;
  {
    base::AutoLock lock(process_state_map_lock_);
    process_state_map_.emplace(process_id, ProcessState(default_priority));
  }
  // It is required to set priority explicitly here because setting thread QoS
  // states requires the process QoS state in advance.
  main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&DBusSchedQOSStateHandler::SetProcessPriorityOnThread,
                     weak_ptr_factory_.GetWeakPtr(), process_id,
                     default_priority));
  main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&DBusSchedQOSStateHandler::SetThreadTypeOnThread,
                     weak_ptr_factory_.GetWeakPtr(), process_id, process_id,
                     base::ThreadType::kDefault));
}

void DBusSchedQOSStateHandler::ForgetProcessPriority(
    base::ProcessId process_id) {
  base::AutoLock lock(process_state_map_lock_);
  if (auto entry = process_state_map_.find(process_id);
      entry != process_state_map_.end()) {
    process_state_map_.erase(entry);
  }
}

bool DBusSchedQOSStateHandler::SetProcessPriority(
    base::ProcessId process_id,
    base::Process::Priority priority) {
  {
    // The overhead of lock should rarely be a problem because most of
    // base::Process::SetPriority() is called from
    // ChildProcessLauncherHelper::SetProcessPriorityOnLauncherThread() which is
    // single-threaded and base::Process::GetPriority() is rarely called.
    base::AutoLock lock(process_state_map_lock_);
    if (auto entry = process_state_map_.find(process_id);
        entry != process_state_map_.end()) {
      entry->second.priority = priority;
    } else {
      // Processes not registered by InitializeProcessPriority() are rejected.
      // Otherwise entries in the map leak after the processes are dead.
      LOG(ERROR) << "process " << process_id
                 << " is not initialized for priority change";
      return false;
    }
  }
  return main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&DBusSchedQOSStateHandler::SetProcessPriorityOnThread,
                     weak_ptr_factory_.GetWeakPtr(), process_id, priority));
}

base::Process::Priority DBusSchedQOSStateHandler::GetProcessPriority(
    base::ProcessId process_id) {
  // Processes in ChromeOS are Priority::kUserBlocking by default.
  base::Process::Priority priority = base::Process::Priority::kUserBlocking;
  {
    base::AutoLock lock(process_state_map_lock_);
    if (auto entry = process_state_map_.find(process_id);
        entry != process_state_map_.end()) {
      priority = entry->second.priority;
    }
  }
  if (priority == base::Process::Priority::kUserVisible) {
    // base::Process::GetPriority() returns either kBestEffort or kUserBlocking.
    priority = base::Process::Priority::kUserBlocking;
  }
  return priority;
}

bool DBusSchedQOSStateHandler::HandleThreadTypeChange(
    base::ProcessId process_id,
    base::PlatformThreadId thread_id,
    base::ThreadType thread_type) {
  {
    base::AutoLock lock(process_state_map_lock_);
    if (auto entry = process_state_map_.find(process_id);
        entry == process_state_map_.end()) {
      LOG(ERROR) << "process " << process_id
                 << " is not initialized for thread type change for thread "
                 << thread_id;
      return false;
    }
  }
  return main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&DBusSchedQOSStateHandler::SetThreadTypeOnThread,
                     weak_ptr_factory_.GetWeakPtr(), process_id, thread_id,
                     thread_type));
}

bool DBusSchedQOSStateHandler::HandleThreadTypeChange(
    base::PlatformThreadId thread_id,
    base::ThreadType thread_type) {
  return HandleThreadTypeChange(getpid(), thread_id, thread_type);
}

void DBusSchedQOSStateHandler::CheckResourcedDisconnected(
    dbus::DBusResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_connected_ && result == dbus::DBusResult::kErrorServiceUnknown) {
    is_connected_ = false;
    ash::ResourcedClient::Get()->WaitForServiceToBeAvailable(
        base::BindOnce(&DBusSchedQOSStateHandler::OnServiceConnected,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void DBusSchedQOSStateHandler::OnServiceConnected(bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!success) {
    LOG(ERROR) << "resourced service is not available";
    return;
  }

  DCHECK(!is_connected_);
  if (is_connected_) {
    LOG(ERROR)
        << "DBusSchedQOSStateHandler::OnServiceConnected while it is connected";
    return;
  }
  is_connected_ = true;

  std::vector<std::pair<base::ProcessId, ProcessState>> preconnect_requests;
  // Copy entries in process_state_map_ to local vector to minimize the
  // locked section.
  // SetProcessPriority() calls just before/after this locked section do not
  // cause incoherence because both OnServiceConnected() and
  // SetProcessPriorityOnThread() run on the same main thread.
  {
    base::AutoLock lock(process_state_map_lock_);
    preconnect_requests.reserve(process_state_map_.size());
    for (auto& entry : process_state_map_) {
      preconnect_requests.push_back(
          {entry.first,
           ProcessState(entry.second.priority, entry.second.need_retry)});
      preconnect_requests.back().second.preconnected_thread_types.swap(
          entry.second.preconnected_thread_types);
      entry.second.need_retry = false;
    }
  }

  for (const auto& request : preconnect_requests) {
    base::ProcessId process_id = request.first;
    if (request.second.need_retry) {
      SetProcessPriorityOnThread(process_id, request.second.priority);
    }
    for (const auto& thread_entry : request.second.preconnected_thread_types) {
      SetThreadTypeOnThread(process_id, thread_entry.first,
                            thread_entry.second);
    }
  }
}

void DBusSchedQOSStateHandler::SetProcessPriorityOnThread(
    base::ProcessId process_id,
    base::Process::Priority priority) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!is_connected_) {
    MarkProcessToRetry(process_id);
    return;
  }

  resource_manager::ProcessState state =
      resource_manager::ProcessState::kNormal;
  switch (priority) {
    case base::Process::Priority::kBestEffort:
      state = resource_manager::ProcessState::kBackground;
      break;
    case base::Process::Priority::kUserBlocking:
    case base::Process::Priority::kUserVisible:
      state = resource_manager::ProcessState::kNormal;
      break;
  }
  base::ElapsedTimer elapsed_timer;
  ash::ResourcedClient::Get()->SetProcessState(
      process_id, state,
      base::BindOnce(&DBusSchedQOSStateHandler::OnSetProcessPriorityFinish,
                     weak_ptr_factory_.GetWeakPtr(), process_id, priority,
                     std::move(elapsed_timer),
                     system::ProcStatFile(process_id)));
}

void DBusSchedQOSStateHandler::OnSetProcessPriorityFinish(
    base::ProcessId process_id,
    base::Process::Priority priority,
    base::ElapsedTimer elapsed_timer,
    system::ProcStatFile stat_file,
    dbus::DBusResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const bool success = result == dbus::DBusResult::kSuccess;
  const bool is_pid_reused =
      IsPidReused(std::move(stat_file), process_id, success);
  LOG_IF(ERROR, is_pid_reused) << "PID reuse detected for pid " << process_id;
  base::UmaHistogramEnumeration(
      "Scheduling.DBusSchedQoS.PidReusedOnSetProcessState",
      GetPidReuseResult(is_pid_reused, success));

  if (success) {
    base::UmaHistogramMicrosecondsTimes(
        "Scheduling.DBusSchedQoS.SetProcessStateLatency",
        elapsed_timer.Elapsed());
  } else {
    LOG(ERROR) << "set process state via resourced failed for pid "
               << process_id << " to " << static_cast<int>(priority)
               << " : DBusResult: " << static_cast<int>(result);
  }

  CheckResourcedDisconnected(result);
  if (!is_connected_) {
    MarkProcessToRetry(process_id);
  }
}

void DBusSchedQOSStateHandler::MarkProcessToRetry(base::ProcessId process_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::AutoLock lock(process_state_map_lock_);
  if (auto entry = process_state_map_.find(process_id);
      entry != process_state_map_.end()) {
    entry->second.need_retry = true;
  }
}

void DBusSchedQOSStateHandler::SetThreadTypeOnThread(
    base::ProcessId process_id,
    base::PlatformThreadId thread_id,
    base::ThreadType thread_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!is_connected_) {
    AddThreadRetryEntry(process_id, thread_id, thread_type);
    return;
  }
  resource_manager::ThreadState state =
      resource_manager::ThreadState::kBalanced;
  switch (thread_type) {
    case base::ThreadType::kBackground:
      state = resource_manager::ThreadState::kBackground;
      break;
    case base::ThreadType::kUtility:
      state = resource_manager::ThreadState::kUtility;
      break;
    case base::ThreadType::kResourceEfficient:
      state = resource_manager::ThreadState::kEco;
      break;
    case base::ThreadType::kDefault:
      state = resource_manager::ThreadState::kBalanced;
      break;
    case base::ThreadType::kDisplayCritical:
      state = resource_manager::ThreadState::kUrgent;
      break;
    case base::ThreadType::kRealtimeAudio:
      state = resource_manager::ThreadState::kUrgentBursty;
      break;
  }
  base::ElapsedTimer elapsed_timer;
  ash::ResourcedClient::Get()->SetThreadState(
      process_id, thread_id, state,
      base::BindOnce(&DBusSchedQOSStateHandler::OnSetThreadTypeFinish,
                     weak_ptr_factory_.GetWeakPtr(), process_id, thread_id,
                     thread_type, std::move(elapsed_timer),
                     system::ProcStatFile(thread_id)));
}

void DBusSchedQOSStateHandler::OnSetThreadTypeFinish(
    base::ProcessId process_id,
    base::PlatformThreadId thread_id,
    base::ThreadType thread_type,
    base::ElapsedTimer elapsed_timer,
    system::ProcStatFile stat_file,
    dbus::DBusResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const bool success = result == dbus::DBusResult::kSuccess;
  const bool is_pid_reused =
      IsPidReused(std::move(stat_file), thread_id, success);
  LOG_IF(ERROR, is_pid_reused)
      << "PID reuse detected for thread id " << thread_id;
  base::UmaHistogramEnumeration(
      "Scheduling.DBusSchedQoS.PidReusedOnSetThreadState",
      GetPidReuseResult(is_pid_reused, success));

  if (result == dbus::DBusResult::kSuccess) {
    base::UmaHistogramMicrosecondsTimes(
        "Scheduling.DBusSchedQoS.SetThreadStateLatency",
        elapsed_timer.Elapsed());
  } else {
    LOG(ERROR) << "set thread state via resourced failed for tid " << thread_id
               << " to " << static_cast<int>(thread_type)
               << " : DBusResult: " << static_cast<int>(result);
  }

  CheckResourcedDisconnected(result);
  if (!is_connected_) {
    AddThreadRetryEntry(process_id, thread_id, thread_type);
  }
}

void DBusSchedQOSStateHandler::AddThreadRetryEntry(
    base::ProcessId process_id,
    base::PlatformThreadId thread_id,
    base::ThreadType thread_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::AutoLock lock(process_state_map_lock_);
  if (auto entry = process_state_map_.find(process_id);
      entry != process_state_map_.end()) {
    entry->second.preconnected_thread_types[thread_id] = thread_type;
  }
}

}  // namespace ash