| // 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/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::kCompositing: |
| 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 |