blob: 794234ba5015f6b11a54bb62debd9169df075fe3 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/power_scheduler/power_scheduler.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/cpu.h"
#include "base/cpu_affinity_posix.h"
#include "base/feature_list.h"
#include "base/lazy_instance.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/process/process_handle.h"
#include "base/process/process_metrics.h"
#include "base/strings/string_split.h"
#include "base/task/current_thread.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "components/power_scheduler/power_scheduler_features.h"
namespace power_scheduler {
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Keep in sync with base::CpuAffinityMode and CpuAffinityMode in enums.xml.
enum class CpuAffinityModeForUma {
kDefault = 0,
kLittleCoresOnly = 1,
kMaxValue = kLittleCoresOnly,
};
CpuAffinityModeForUma GetCpuAffinityModeForUma(base::CpuAffinityMode affinity) {
switch (affinity) {
case base::CpuAffinityMode::kDefault:
return CpuAffinityModeForUma::kDefault;
case base::CpuAffinityMode::kLittleCoresOnly:
return CpuAffinityModeForUma::kLittleCoresOnly;
}
}
perfetto::StaticString TraceEventNameForAffinityMode(
base::CpuAffinityMode affinity) {
switch (affinity) {
case base::CpuAffinityMode::kDefault:
return "ApplyCpuAffinityModeDefault";
case base::CpuAffinityMode::kLittleCoresOnly:
return "ApplyCpuAffinityModeLittleCoresOnly";
}
}
void ApplyProcessCpuAffinityMode(base::CpuAffinityMode affinity) {
TRACE_EVENT("power", TraceEventNameForAffinityMode(affinity));
// Restrict affinity of all existing threads of the current process. The
// affinity is inherited by any subsequently created thread. Other threads may
// already exist even during early startup (e.g. Java threads like
// RenderThread), so setting the affinity only for the current thread is not
// enough here.
bool success = base::SetProcessCpuAffinityMode(
base::GetCurrentProcessHandle(), affinity);
base::UmaHistogramBoolean(
"Power.CpuAffinityExperiments.ProcessAffinityUpdateSuccess", success);
if (success) {
base::UmaHistogramEnumeration(
"Power.CpuAffinityExperiments.ProcessAffinityMode",
GetCpuAffinityModeForUma(affinity));
}
}
bool CpuAffinityApplicable() {
// For now, affinity modes only have an effect on big.LITTLE architectures.
return base::HasBigCpuCores();
}
// Default policy params for the PowerScheduler feature. Please update the
// comment in power_scheduler_features.cc when changing these defaults.
static constexpr SchedulingPolicyParams kDefaultParams{
SchedulingPolicy::kThrottleIdleAndNopAnimation,
base::TimeDelta::FromMilliseconds(500), 0.5f};
// Keys/values for the field trial params.
static const char kPolicyKey[] = "policy";
static const char kPolicyLittleCoresOnly[] = "kLittleCoresOnly";
static const char kPolicyThrottleIdle[] = "kThrottleIdle";
static const char kPolicyThrottleIdleAndNopAnimation[] =
"kThrottleIdleAndNopAnimation";
static const char kMinTimeInModeMsKey[] = "min_time_in_mode_ms";
static const char kMinCputimeRatioKey[] = "min_cputime_ratio";
static const char kProcessTypesKey[] = "process_types";
static const char kIncludeChargingKey[] = "include_charging";
} // anonymous namespace
PowerScheduler::PowerScheduler(PowerModeArbiter* arbiter)
: arbiter_(arbiter),
process_metrics_(base::ProcessMetrics::CreateCurrentProcessMetrics()) {
DETACH_FROM_SEQUENCE(thread_pool_checker_);
}
PowerScheduler::~PowerScheduler() = default;
// static
PowerScheduler* PowerScheduler::GetInstance() {
static base::NoDestructor<PowerScheduler> instance(
PowerModeArbiter::GetInstance());
return instance.get();
}
void PowerScheduler::WillProcessTask(const base::PendingTask& pending_task,
bool was_blocked_or_low_priority) {}
void PowerScheduler::DidProcessTask(const base::PendingTask& pending_task) {
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
++task_counter_;
if (task_counter_ == kUpdateAfterEveryNTasks) {
thread_pool_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&PowerScheduler::EnforceCpuAffinityOnSequence,
base::Unretained(this) // never destroyed.
));
task_counter_ = 0;
}
}
void PowerScheduler::OnPowerModeChanged(PowerMode old_mode,
PowerMode new_mode) {
TRACE_EVENT2("power", "PowerScheduler::OnPowerModeChanged", "old_mode",
PowerModeToString(old_mode), "new_mode",
PowerModeToString(new_mode));
OnPowerModeChangedOnSequence(old_mode, new_mode);
}
void PowerScheduler::Setup() {
// The setup should be called once from the main thread. In single-process
// mode, it may later be called on other threads (which should be ignored).
if (did_call_setup_)
return;
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
did_call_setup_ = true;
SetupTaskRunners(base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
}
void PowerScheduler::SetupTaskRunners(
scoped_refptr<base::TaskRunner> thread_pool_task_runner) {
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
main_thread_task_runner_ = base::SequencedTaskRunnerHandle::Get();
thread_pool_task_runner_ = thread_pool_task_runner;
if (pending_policy_.policy == SchedulingPolicy::kNone)
return;
thread_pool_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&PowerScheduler::SetupPolicyOnSequence,
base::Unretained(this), // never destroyed.
pending_policy_));
pending_policy_ = SchedulingPolicyParams();
}
base::TimeDelta PowerScheduler::GetProcessCpuTime() {
return process_metrics_->GetCumulativeCPUUsage();
}
void PowerScheduler::InitializePolicyFromFeatureList() {
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
// To preserve earlier behavior of the field trials, check for the legacy
// features before checking if throttling is supported.
if (base::FeatureList::IsEnabled(
features::kCpuAffinityRestrictToLittleCores)) {
SetPolicy(SchedulingPolicy::kLittleCoresOnly);
return;
}
if (base::FeatureList::IsEnabled(
features::kPowerSchedulerThrottleIdleAndNopAnimation)) {
SetPolicy(SchedulingPolicy::kThrottleIdleAndNopAnimation);
return;
}
if (base::FeatureList::IsEnabled(features::kPowerSchedulerThrottleIdle)) {
SetPolicy(SchedulingPolicy::kThrottleIdle);
return;
}
if (base::FeatureList::IsEnabled(
features::kWebViewCpuAffinityRestrictToLittleCores)) {
SetPolicy(SchedulingPolicy::kLittleCoresOnly);
return;
}
if (base::FeatureList::IsEnabled(
features::kWebViewPowerSchedulerThrottleIdle)) {
SetPolicy(SchedulingPolicy::kThrottleIdle);
return;
}
// Only check for the new feature after checking that throttling is supported.
if (!CpuAffinityApplicable())
return;
if (!base::FeatureList::IsEnabled(features::kPowerScheduler))
return;
SchedulingPolicyParams params = kDefaultParams;
std::map<std::string, std::string> field_trial_params;
if (base::GetFieldTrialParamsByFeature(features::kPowerScheduler,
&field_trial_params)) {
auto policy_it = field_trial_params.find(kPolicyKey);
if (policy_it != field_trial_params.end()) {
if (policy_it->second == kPolicyLittleCoresOnly) {
params.policy = SchedulingPolicy::kLittleCoresOnly;
} else if (policy_it->second == kPolicyThrottleIdle) {
params.policy = SchedulingPolicy::kThrottleIdle;
} else if (policy_it->second == kPolicyThrottleIdleAndNopAnimation) {
params.policy = SchedulingPolicy::kThrottleIdleAndNopAnimation;
}
}
int min_time_ms = 0;
if (base::StringToInt(field_trial_params[kMinTimeInModeMsKey],
&min_time_ms)) {
params.min_time_in_mode = base::TimeDelta::FromMilliseconds(min_time_ms);
}
double min_cputime_ratio = 0;
if (base::StringToDouble(field_trial_params[kMinCputimeRatioKey],
&min_cputime_ratio)) {
params.min_cputime_ratio = min_cputime_ratio;
}
// If there is a process type allowlist, check if the current process's type
// is in it. Otherwise don't enable power scheduling.
const std::string& process_types = field_trial_params[kProcessTypesKey];
if (!process_types.empty()) {
std::vector<std::string> split = base::SplitString(
process_types, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
std::string process_type =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII("type");
if (process_type.empty()) {
if (!base::Contains(split, "browser"))
return;
} else if (!base::Contains(split, process_type)) {
return;
}
}
bool include_charging = field_trial_params[kIncludeChargingKey] == "true";
if (include_charging)
arbiter_->SetChargingModeEnabled(false);
}
SetPolicy(params);
}
void PowerScheduler::SetPolicy(SchedulingPolicy policy) {
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
SchedulingPolicyParams params;
params.policy = policy;
SetPolicy(params);
}
void PowerScheduler::SetPolicy(SchedulingPolicyParams policy) {
DCHECK_CALLED_ON_VALID_SEQUENCE(main_thread_checker_);
if (!CpuAffinityApplicable())
return;
// Little-only policy only makes sense without minimum times.
if (policy.policy == SchedulingPolicy::kLittleCoresOnly) {
policy.min_time_in_mode = base::TimeDelta();
policy.min_cputime_ratio = 0;
}
// Set up the power affinity observer and apply the policy if it's already
// possible. Otherwise it will be set up after thread initialization via
// Setup() (see app/content_main_runner_impl.cc and child/child_process.cc).
if (thread_pool_task_runner_) {
thread_pool_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&PowerScheduler::SetupPolicyOnSequence,
base::Unretained(this), // never destroyed.
policy));
} else {
pending_policy_ = policy;
}
}
void PowerScheduler::SetupPolicyOnSequence(SchedulingPolicyParams policy) {
DCHECK_CALLED_ON_VALID_SEQUENCE(thread_pool_checker_);
// Reset the power mode in case it contains an obsolete value.
current_power_mode_ = PowerMode::kMaxValue;
current_policy_ = policy;
bool needs_power_observer =
current_policy_.policy == SchedulingPolicy::kThrottleIdle ||
current_policy_.policy == SchedulingPolicy::kThrottleIdleAndNopAnimation;
if (needs_power_observer && !power_observer_registered_) {
arbiter_->AddObserver(this);
power_observer_registered_ = true;
} else if (!needs_power_observer && power_observer_registered_) {
arbiter_->RemoveObserver(this);
power_observer_registered_ = false;
}
ApplyPolicyOnSequence();
}
void PowerScheduler::OnPowerModeChangedOnSequence(PowerMode old_mode,
PowerMode new_mode) {
DCHECK_CALLED_ON_VALID_SEQUENCE(thread_pool_checker_);
current_power_mode_ = new_mode;
ApplyPolicyOnSequence();
}
base::CpuAffinityMode PowerScheduler::GetTargetCpuAffinity() {
DCHECK_CALLED_ON_VALID_SEQUENCE(thread_pool_checker_);
bool is_throttleable_mode = false;
switch (current_policy_.policy) {
case SchedulingPolicy::kNone:
break;
case SchedulingPolicy::kLittleCoresOnly:
is_throttleable_mode = true;
break;
case SchedulingPolicy::kThrottleIdle:
is_throttleable_mode = current_power_mode_ == PowerMode::kIdle ||
current_power_mode_ == PowerMode::kBackground;
break;
case SchedulingPolicy::kThrottleIdleAndNopAnimation:
is_throttleable_mode = current_power_mode_ == PowerMode::kIdle ||
current_power_mode_ == PowerMode::kBackground ||
current_power_mode_ == PowerMode::kNopAnimation;
break;
}
bool currently_throttling =
enforced_affinity_ == base::CpuAffinityMode::kLittleCoresOnly;
if (is_throttleable_mode == currently_throttling) {
time_entered_throttleable_mode_ = base::TimeTicks();
cputime_entered_throttleable_mode_ = base::TimeDelta();
return enforced_affinity_;
}
// If we are in a throttleable mode, check if we've been in a throttleable
// mode for long enough and consumed enough CPU. Otherwise, don't change
// anything (yet) and schedule a follow-up check if needed.
if (is_throttleable_mode &&
current_policy_.min_time_in_mode > base::TimeDelta()) {
base::TimeTicks now = base::TimeTicks::Now();
base::TimeDelta cumulative_cpu = GetProcessCpuTime();
if (time_entered_throttleable_mode_.is_null()) {
time_entered_throttleable_mode_ = now;
cputime_entered_throttleable_mode_ = cumulative_cpu;
thread_pool_task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&PowerScheduler::ApplyPolicyOnSequence,
base::Unretained(this)), // never destroyed.
current_policy_.min_time_in_mode);
return enforced_affinity_;
} else {
bool minimums_exceeded = false;
base::TimeDelta time_elapsed = now - time_entered_throttleable_mode_;
base::TimeDelta cputime_elapsed =
cumulative_cpu - cputime_entered_throttleable_mode_;
minimums_exceeded =
time_elapsed >= current_policy_.min_time_in_mode &&
cputime_elapsed >= time_elapsed * current_policy_.min_cputime_ratio;
if (!minimums_exceeded) {
TRACE_EVENT_INSTANT2("power", "PowerScheduler.MinimumsNotExceeded",
TRACE_EVENT_SCOPE_THREAD, "time_elapsed_ms",
time_elapsed.InMilliseconds(),
"cputime_elapsed_ms",
cputime_elapsed.InMilliseconds());
return enforced_affinity_;
} else {
TRACE_EVENT_INSTANT2("power", "PowerScheduler.MinimumsExceeded",
TRACE_EVENT_SCOPE_THREAD, "time_elapsed_ms",
time_elapsed.InMilliseconds(),
"cputime_elapsed_ms",
cputime_elapsed.InMilliseconds());
}
}
} else {
time_entered_throttleable_mode_ = base::TimeTicks();
cputime_entered_throttleable_mode_ = base::TimeDelta();
}
return is_throttleable_mode ? base::CpuAffinityMode::kLittleCoresOnly
: base::CpuAffinityMode::kDefault;
}
void PowerScheduler::ApplyPolicyOnSequence() {
DCHECK_CALLED_ON_VALID_SEQUENCE(thread_pool_checker_);
TRACE_EVENT0("power", "PowerScheduler::ApplyPolicyOnSequence");
base::CpuAffinityMode target_affinity = GetTargetCpuAffinity();
if (target_affinity == enforced_affinity_)
return;
base::TimeTicks now = base::TimeTicks::Now();
if (target_affinity == base::CpuAffinityMode::kDefault &&
!enforced_affinity_setup_time_.is_null()) {
auto throttling_duration = now - enforced_affinity_setup_time_;
UMA_HISTOGRAM_CUSTOM_TIMES("Power.PowerScheduler.ThrottlingDuration",
throttling_duration,
base::TimeDelta::FromMilliseconds(1),
base::TimeDelta::FromMinutes(10), 100);
UMA_HISTOGRAM_SCALED_ENUMERATION(
"Power.PowerScheduler.ThrottlingDurationPerCpuAffinityMode",
GetCpuAffinityModeForUma(enforced_affinity_),
throttling_duration.InMicroseconds(),
base::Time::kMicrosecondsPerMillisecond);
}
if (target_affinity == base::CpuAffinityMode::kLittleCoresOnly) {
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("power", "PowerScheduler.LittleCoresOnly",
this);
} else {
TRACE_EVENT_NESTABLE_ASYNC_END0("power", "PowerScheduler.LittleCoresOnly",
this);
}
enforced_affinity_setup_time_ = now;
enforced_affinity_ = target_affinity;
TRACE_EVENT_INSTANT1("power", "PowerScheduler.NewAffinity",
TRACE_EVENT_SCOPE_THREAD, "enforced_affinity",
enforced_affinity_);
EnforceCpuAffinityOnSequence();
}
void PowerScheduler::EnforceCpuAffinityOnSequence() {
DCHECK_CALLED_ON_VALID_SEQUENCE(thread_pool_checker_);
if (enforced_affinity_ != base::CurrentThreadCpuAffinityMode())
ApplyProcessCpuAffinityMode(enforced_affinity_);
// Android system may reset a non-default affinity setting, so we need to
// check periodically if we need to re-apply it.
bool mode_needs_periodic_enforcement =
enforced_affinity_ != base::CpuAffinityMode::kDefault;
if (mode_needs_periodic_enforcement && !task_observer_registered_) {
main_thread_task_runner_->PostTask(
FROM_HERE, base::BindOnce(
[](PowerScheduler* scheduler) {
base::CurrentThread::Get()->AddTaskObserver(scheduler);
},
base::Unretained(this))); // never deleted.
task_observer_registered_ = true;
} else if (!mode_needs_periodic_enforcement && task_observer_registered_) {
main_thread_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](PowerScheduler* scheduler) {
base::CurrentThread::Get()->RemoveTaskObserver(scheduler);
scheduler->task_counter_ = 0;
},
base::Unretained(this))); // never deleted.
task_observer_registered_ = false;
}
}
} // namespace power_scheduler