// 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 "base/android/pre_freeze_background_memory_trimmer.h" #include #include #include #include #include "base/android/build_info.h" #include "base/android/pmf_utils.h" #include "base/cancelable_callback.h" #include "base/check.h" #include "base/command_line.h" #include "base/feature_list.h" #include "base/functional/bind.h" #include "base/logging.h" #include "base/memory/page_size.h" #include "base/metrics/histogram_functions.h" #include "base/strings/strcat.h" #include "base/task/sequenced_task_runner.h" #include "base/task/thread_pool.h" #include "base/task/thread_pool/thread_pool_instance.h" #include "base/time/time.h" #include "base/trace_event/base_tracing.h" namespace base::android { namespace { // These values are logged to UMA. Entries should not be renumbered and // numeric values should never be reused. Please keep in sync with // "PreFreezeMetricsFailureType" in tools/metrics/histograms/enums.xml. enum class MetricsFailure { kAlreadyRunning, kSizeMismatch, kMaxValue = kSizeMismatch }; // This constant is chosen arbitrarily, to allow time for the background tasks // to finish running BEFORE collecting metrics. const base::TimeDelta kDelayForMetrics = base::Seconds(2); uint64_t BytesToMiB(uint64_t v) { return v / 1024 / 1024; } const char* GetProcessType() { CHECK(base::CommandLine::InitializedForCurrentProcess()); const std::string type = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII("type"); const char* process_type = type == "" ? "Browser" : type == "renderer" ? "Renderer" : type == "gpu-process" ? "GPU" : type == "utility" ? "Utility" : "Unknown"; return process_type; } std::string GetMetricName(std::string_view name, std::string_view suffix) { const char* process_type = GetProcessType(); return StrCat({"Memory.PreFreeze2.", process_type, ".", name, ".", suffix}); } class PrivateMemoryFootprintMetric : public PreFreezeBackgroundMemoryTrimmer::PreFreezeMetric { public: PrivateMemoryFootprintMetric() : PreFreezeBackgroundMemoryTrimmer::PreFreezeMetric( "PrivateMemoryFootprint") {} std::optional Measure() const override { return PmfUtils::GetPrivateMemoryFootprintForCurrentProcess(); } ~PrivateMemoryFootprintMetric() override = default; // Whether the metric has been registered with // |PreFreezeBackgroundMemoryTrimmer| or not, which happens the first time a // task is posted via |PreFreezeBackgroundMemoryTrimmer| or // |OneShotDelayedBackgroundTimer|. static bool did_register_; }; bool PrivateMemoryFootprintMetric::did_register_ = false; void MaybeRecordMetric(std::optional value_bytes, std::string_view metric_name, std::string_view suffix) { // Skip recording the metric if we failed to get the PMF. if (!value_bytes.has_value()) { return; } UmaHistogramMemoryMB(GetMetricName(metric_name, suffix), static_cast(BytesToMiB(value_bytes.value()))); } std::optional Diff(std::optional before, std::optional after) { if (!before.has_value() || !before.has_value()) { return std::nullopt; } const uint64_t before_value = before.value(); const uint64_t after_value = after.value(); return after_value < before_value ? before_value - after_value : 0; } bool IsMadvisePageoutSupported() { static bool supported = []() -> bool { #if defined(MADV_PAGEOUT) // To determine if MADV_PAGEOUT is supported we will try calling it with an // invalid memory area. // madvise(2) first checks the mode first, returning -EINVAL if it's // unknown. Next, it will always return 0 for a zero length VMA before // validating if it's mapped. // So, in this case, we can test for support with any page aligned address // with a zero length. int res = madvise(reinterpret_cast(base::GetPageSize()), 0, MADV_PAGEOUT); if (res < 0 && errno == -EINVAL) return false; PLOG_IF(ERROR, res < 0) << "Unexpected return from madvise"; if (res == 0) return true; #endif return false; }(); return supported; } } // namespace BASE_FEATURE(kOnPreFreezeMemoryTrim, "OnPreFreezeMemoryTrim", FEATURE_ENABLED_BY_DEFAULT); BASE_FEATURE(kIsTrimMemoryBackgroundCritical, "IsTrimMemoryBackgroundCritical", FEATURE_ENABLED_BY_DEFAULT); BASE_FEATURE(kShouldFreezeSelf, "ShouldFreezeSelf", FEATURE_DISABLED_BY_DEFAULT); PreFreezeBackgroundMemoryTrimmer::PreFreezeBackgroundMemoryTrimmer() : supports_modern_trim_(BuildInfo::GetInstance()->sdk_int() >= SDK_VERSION_U) {} // static PreFreezeBackgroundMemoryTrimmer& PreFreezeBackgroundMemoryTrimmer::Instance() { static base::NoDestructor instance; return *instance; } void PreFreezeBackgroundMemoryTrimmer::RecordMetrics() { // We check that the command line is available here because we use it to // determine the current process, which is used for the names of metrics // below. CHECK(base::CommandLine::InitializedForCurrentProcess()); base::AutoLock locker(lock_); if (metrics_.size() != values_before_.size()) { UmaHistogramEnumeration("Memory.PreFreeze2.RecordMetricsFailureType", MetricsFailure::kSizeMismatch); values_before_.clear(); return; } for (size_t i = 0; i < metrics_.size(); i++) { const auto metric = metrics_[i]; const std::optional value_before = values_before_[i]; std::optional value_after = metric->Measure(); MaybeRecordMetric(value_before, metric->name(), "Before"); MaybeRecordMetric(value_after, metric->name(), "After"); MaybeRecordMetric(Diff(value_before, value_after), metric->name(), "Diff"); } values_before_.clear(); } void PreFreezeBackgroundMemoryTrimmer::PostMetricsTask() { // PreFreeze is only for Android U and greater, so no need to record metrics // for older versions. if (!SupportsModernTrim()) { return; } // We need the process type to record the metrics below, which we get from // the command line. We cannot post the task below if the thread pool is not // initialized yet. if (!base::CommandLine::InitializedForCurrentProcess() || !base::ThreadPoolInstance::Get()) { return; } // The |RecordMetrics| task resets the |values_before_| after it uses them. // That task is posted with a 2 second delay from when |OnPreFreeze| is run. // // From the time that Chrome is backgrounded until Android delivers the signal // to run PreFreeze always takes at least 10 seconds. // // Therefore, even if we: // - Post |RecordMetrics| // - and then immediately return to foreground and immediately back to // background. // We still will have to wait at least 10 seconds before we get the PreFreeze // signal again, by which time the original RecordMetrics task will have // already finished. if (values_before_.size() > 0) { UmaHistogramEnumeration("Memory.PreFreeze2.RecordMetricsFailureType", MetricsFailure::kAlreadyRunning); return; } for (const auto& metric : metrics_) { values_before_.push_back(metric->Measure()); } // The posted task will be more likely to survive background killing in // experiments that change the memory trimming behavior. Run as USER_BLOCKING // to reduce this sample imbalance in experiment groups. Normally tasks // collecting metrics should use BEST_EFFORT, but when running in background a // number of subtle effects may influence the real delay of those tasks. The // USER_BLOCKING will allow to estimate the number of better-survived tasks // more precisely. base::ThreadPool::PostDelayedTask( FROM_HERE, {base::TaskPriority::USER_BLOCKING, MayBlock()}, base::BindOnce(&PreFreezeBackgroundMemoryTrimmer::RecordMetrics, base::Unretained(this)), kDelayForMetrics); } // static void PreFreezeBackgroundMemoryTrimmer::PostDelayedBackgroundTask( scoped_refptr task_runner, const base::Location& from_here, OnceCallback task, base::TimeDelta delay) { // Preserve previous behaviour on versions before Android U. if (!SupportsModernTrim()) { task_runner->PostDelayedTask( from_here, BindOnce(std::move(task), MemoryReductionTaskContext::kDelayExpired), delay); return; } Instance().PostDelayedBackgroundTaskInternal(task_runner, from_here, std::move(task), delay); } void PreFreezeBackgroundMemoryTrimmer::PostDelayedBackgroundTaskInternal( scoped_refptr task_runner, const base::Location& from_here, OnceCallback task, base::TimeDelta delay) { DCHECK(SupportsModernTrim()); RegisterPrivateMemoryFootprintMetric(); if (!base::FeatureList::IsEnabled(kOnPreFreezeMemoryTrim)) { task_runner->PostDelayedTask( from_here, BindOnce(std::move(task), MemoryReductionTaskContext::kDelayExpired), delay); return; } PostDelayedBackgroundTaskModern(task_runner, from_here, std::move(task), delay); } void PreFreezeBackgroundMemoryTrimmer::PostDelayedBackgroundTaskModern( scoped_refptr task_runner, const base::Location& from_here, OnceCallback task, base::TimeDelta delay) { // We create a cancellable delayed task (below), which must be done on the // same TaskRunner that will run the task eventually, so we may need to // repost this on the correct TaskRunner. if (!task_runner->RunsTasksInCurrentSequence()) { // |base::Unretained(this)| is safe here because we never destroy |this|. task_runner->PostTask( FROM_HERE, base::BindOnce( &PreFreezeBackgroundMemoryTrimmer::PostDelayedBackgroundTaskModern, base::Unretained(this), task_runner, from_here, std::move(task), delay)); return; } base::AutoLock locker(lock_); PostDelayedBackgroundTaskModernHelper(std::move(task_runner), from_here, std::move(task), delay); } PreFreezeBackgroundMemoryTrimmer::BackgroundTask* PreFreezeBackgroundMemoryTrimmer::PostDelayedBackgroundTaskModernHelper( scoped_refptr task_runner, const Location& from_here, OnceCallback task, TimeDelta delay) { std::unique_ptr background_task = BackgroundTask::Create(task_runner, from_here, std::move(task), delay); auto* ptr = background_task.get(); background_tasks_.push_back(std::move(background_task)); return ptr; } // static void PreFreezeBackgroundMemoryTrimmer::RegisterMemoryMetric( const PreFreezeMetric* metric) { base::AutoLock locker(Instance().lock_); Instance().RegisterMemoryMetricInternal(metric); } void PreFreezeBackgroundMemoryTrimmer::RegisterMemoryMetricInternal( const PreFreezeMetric* metric) { metrics_.push_back(metric); // If we are in the middle of recording metrics when we register this, add // a nullopt at the end so that metrics recording doesn't fail for all // metrics, just this one. if (values_before_.size() > 0) { values_before_.push_back(std::nullopt); } } // static void PreFreezeBackgroundMemoryTrimmer::UnregisterMemoryMetric( const PreFreezeMetric* metric) { base::AutoLock locker(Instance().lock_); Instance().UnregisterMemoryMetricInternal(metric); } void PreFreezeBackgroundMemoryTrimmer::UnregisterMemoryMetricInternal( const PreFreezeMetric* metric) { auto it = std::find(metrics_.begin(), metrics_.end(), metric); CHECK(it != metrics_.end()); const long index = it - metrics_.begin(); if (values_before_.size() > 0) { CHECK_EQ(values_before_.size(), metrics_.size()); values_before_.erase(values_before_.begin() + index); } metrics_.erase(metrics_.begin() + index); } // static bool PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported() { return IsMadvisePageoutSupported(); } // static std::optional PreFreezeBackgroundMemoryTrimmer::CompactSelf() { // MADV_PAGEOUT was only added in Linux 5.4, so do nothing in earlier // versions. if (!SelfCompactionIsSupported()) { return std::nullopt; } std::vector regions; std::string proc_maps; if (!debug::ReadProcMaps(&proc_maps) || !ParseProcMaps(proc_maps, ®ions)) { return std::nullopt; } if (regions.size() == 0) { return std::nullopt; } // TODO(crbug.com/344547190): This may run for a long time. Add a way to // cancel this part-way through if we return to the foreground while this is // running. return CompactMemory(std::move(regions)); } // static std::optional PreFreezeBackgroundMemoryTrimmer::CompactRegion( debug::MappedMemoryRegion region) { #if defined(MADV_PAGEOUT) // Skip file-backed regions if (region.inode != 0 || region.dev_major != 0) { return 0; } // Skip shared regions if ((region.permissions & debug::MappedMemoryRegion::Permission::PRIVATE) == 0) { return 0; } TRACE_EVENT1("base", __PRETTY_FUNCTION__, "size", region.end - region.start); int error = madvise(reinterpret_cast(region.start), region.end - region.start, MADV_PAGEOUT); if (error < 0) { // We may fail on some regions, such as [vvar], or a locked region. It's // not worth it to try to filter these all out, so we just skip them, and // rely on metrics to verify that this is working correctly for most // regions. // // EINVAL could be [vvar] or a locked region. ENOMEM would be a moved or // unmapped region. if (errno != EINVAL && errno != ENOMEM) { PLOG(ERROR) << "Unexpected error from madvise."; return std::nullopt; } return 0; } return region.end - region.start; #else return std::nullopt; #endif } // static std::optional PreFreezeBackgroundMemoryTrimmer::CompactMemory( std::vector regions) { TRACE_EVENT1("base", __PRETTY_FUNCTION__, "count", regions.size()); int64_t total_bytes_processed = 0; for (const auto& region : regions) { const auto bytes_processed = CompactRegion(region); if (!bytes_processed) { return std::nullopt; } total_bytes_processed += bytes_processed.value(); } return total_bytes_processed; } void PreFreezeBackgroundMemoryTrimmer::PostMetricsTasksIfModern() { if (!SupportsModernTrim()) { return; } PostMetricsTask(); } // static void PreFreezeBackgroundMemoryTrimmer::OnSelfFreeze() { // TODO } // static void PreFreezeBackgroundMemoryTrimmer::OnPreFreeze() { Instance().OnPreFreezeInternal(); } void PreFreezeBackgroundMemoryTrimmer::OnPreFreezeInternal() { base::AutoLock locker(lock_); PostMetricsTasksIfModern(); if (!ShouldUseModernTrim()) { return; } // We check |num_pending_tasks-- > 0| so that we have an upper limit on the // number of tasks that we run. // We check |!background_tasks_.empty()| so that we exit as soon as we have // no more tasks to run. // // This handles both the case where we have tasks that post other tasks (we // won't run endlessly because of the upper limit), and the case where tasks // cancel other tasks (we exit as soon as the queue is empty). // // Note that the current implementation may run some tasks that were posted // by earlier tasks, if some other tasks are also cancelled, but we // stop eventually due to the upper limit. size_t num_pending_tasks = background_tasks_.size(); while (num_pending_tasks-- > 0 && !background_tasks_.empty()) { auto background_task = std::move(background_tasks_.front()); background_tasks_.pop_front(); // We release the lock here for two reasons: // (1) To avoid holding it too long while running all the background tasks. // (2) To prevent a deadlock if the |background_task| needs to acquire the // lock (e.g. to post another task). base::AutoUnlock unlocker(lock_); BackgroundTask::RunNow(std::move(background_task)); } } // static void PreFreezeBackgroundMemoryTrimmer::UnregisterBackgroundTask( BackgroundTask* task) { return Instance().UnregisterBackgroundTaskInternal(task); } void PreFreezeBackgroundMemoryTrimmer::UnregisterBackgroundTaskInternal( BackgroundTask* timer) { base::AutoLock locker(lock_); std::erase_if(background_tasks_, [&](auto& t) { return t.get() == timer; }); } // static void PreFreezeBackgroundMemoryTrimmer::RegisterPrivateMemoryFootprintMetric() { base::AutoLock locker(Instance().lock_); static base::NoDestructor pmf_metric; if (!PrivateMemoryFootprintMetric::did_register_) { PrivateMemoryFootprintMetric::did_register_ = true; Instance().RegisterMemoryMetricInternal(pmf_metric.get()); } } // static bool PreFreezeBackgroundMemoryTrimmer::SupportsModernTrim() { return Instance().supports_modern_trim_; } // static bool PreFreezeBackgroundMemoryTrimmer::ShouldUseModernTrim() { return SupportsModernTrim() && base::FeatureList::IsEnabled(kOnPreFreezeMemoryTrim); } // static bool PreFreezeBackgroundMemoryTrimmer::IsTrimMemoryBackgroundCritical() { return SupportsModernTrim() && base::FeatureList::IsEnabled(kIsTrimMemoryBackgroundCritical); } // static void PreFreezeBackgroundMemoryTrimmer::SetSupportsModernTrimForTesting( bool is_supported) { Instance().supports_modern_trim_ = is_supported; } // static void PreFreezeBackgroundMemoryTrimmer::ClearMetricsForTesting() { base::AutoLock locker(Instance().lock_); Instance().metrics_.clear(); PrivateMemoryFootprintMetric::did_register_ = false; } bool PreFreezeBackgroundMemoryTrimmer::DidRegisterTasksForTesting() const { base::AutoLock locker(lock_); return metrics_.size() != 0; } size_t PreFreezeBackgroundMemoryTrimmer::GetNumberOfPendingBackgroundTasksForTesting() const { base::AutoLock locker(lock_); return background_tasks_.size(); } size_t PreFreezeBackgroundMemoryTrimmer::GetNumberOfKnownMetricsForTesting() const { base::AutoLock locker(lock_); return metrics_.size(); } size_t PreFreezeBackgroundMemoryTrimmer::GetNumberOfValuesBeforeForTesting() const { base::AutoLock locker(lock_); return values_before_.size(); } // static void PreFreezeBackgroundMemoryTrimmer::BackgroundTask::RunNow( std::unique_ptr background_task) { if (!background_task->task_runner_->RunsTasksInCurrentSequence()) { background_task->task_runner_->PostTask( FROM_HERE, base::BindOnce(&BackgroundTask::RunNow, std::move(background_task))); return; } // We check that the task has not been run already. If it has, we do not run // it again. if (background_task->task_handle_.IsValid()) { background_task->task_handle_.CancelTask(); } else { return; } background_task->Run(MemoryReductionTaskContext::kProactive); } void PreFreezeBackgroundMemoryTrimmer::BackgroundTask::CancelTask() { if (task_handle_.IsValid()) { task_handle_.CancelTask(); PreFreezeBackgroundMemoryTrimmer::UnregisterBackgroundTask(this); } } // static std::unique_ptr PreFreezeBackgroundMemoryTrimmer::BackgroundTask::Create( scoped_refptr task_runner, const base::Location& from_here, OnceCallback task, base::TimeDelta delay) { DCHECK(task_runner->RunsTasksInCurrentSequence()); auto background_task = std::make_unique(task_runner); background_task->Start(from_here, delay, std::move(task)); return background_task; } PreFreezeBackgroundMemoryTrimmer::BackgroundTask::BackgroundTask( scoped_refptr task_runner) : task_runner_(task_runner) {} PreFreezeBackgroundMemoryTrimmer::BackgroundTask::~BackgroundTask() = default; void PreFreezeBackgroundMemoryTrimmer::BackgroundTask::Run( MemoryReductionTaskContext from_pre_freeze) { DCHECK(!task_handle_.IsValid()); std::move(task_).Run(from_pre_freeze); } void PreFreezeBackgroundMemoryTrimmer::BackgroundTask::Start( const base::Location& from_here, base::TimeDelta delay, OnceCallback task) { task_ = std::move(task); task_handle_ = task_runner_->PostCancelableDelayedTask( subtle::PostDelayedTaskPassKey(), from_here, base::BindOnce( [](BackgroundTask* p) { p->Run(MemoryReductionTaskContext::kDelayExpired); UnregisterBackgroundTask(p); }, this), delay); } PreFreezeBackgroundMemoryTrimmer::PreFreezeMetric::PreFreezeMetric( const std::string& name) : name_(name) {} PreFreezeBackgroundMemoryTrimmer::PreFreezeMetric::~PreFreezeMetric() = default; } // namespace base::android