| // Copyright 2017 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 "base/trace_event/memory_peak_detector.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/sys_info.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/memory_dump_manager.h" |
| #include "base/trace_event/memory_dump_provider_info.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| |
| namespace base { |
| namespace trace_event { |
| |
| // static |
| MemoryPeakDetector* MemoryPeakDetector::GetInstance() { |
| static MemoryPeakDetector* instance = new MemoryPeakDetector(); |
| return instance; |
| } |
| |
| MemoryPeakDetector::MemoryPeakDetector() |
| : generation_(0), |
| state_(NOT_INITIALIZED), |
| poll_tasks_count_for_testing_(0) {} |
| |
| MemoryPeakDetector::~MemoryPeakDetector() { |
| // This is hit only in tests, in which case the test is expected to TearDown() |
| // cleanly and not leave the peak detector running. |
| DCHECK_EQ(NOT_INITIALIZED, state_); |
| } |
| |
| void MemoryPeakDetector::Setup( |
| const GetDumpProvidersFunction& get_dump_providers_function, |
| const scoped_refptr<SequencedTaskRunner>& task_runner, |
| const OnPeakDetectedCallback& on_peak_detected_callback) { |
| DCHECK(!get_dump_providers_function.is_null()); |
| DCHECK(task_runner); |
| DCHECK(!on_peak_detected_callback.is_null()); |
| DCHECK(state_ == NOT_INITIALIZED || state_ == DISABLED); |
| DCHECK(dump_providers_.empty()); |
| get_dump_providers_function_ = get_dump_providers_function; |
| task_runner_ = task_runner; |
| on_peak_detected_callback_ = on_peak_detected_callback; |
| state_ = DISABLED; |
| config_ = {}; |
| ResetPollHistory(); |
| |
| static_threshold_bytes_ = 0; |
| #if !defined(OS_NACL) |
| // Set threshold to 1% of total system memory. |
| static_threshold_bytes_ = |
| static_cast<uint64_t>(SysInfo::AmountOfPhysicalMemory()) / 100; |
| #endif |
| // Fallback, mostly for test environments where AmountOfPhysicalMemory() is |
| // broken. |
| static_threshold_bytes_ = |
| std::max(static_threshold_bytes_, static_cast<uint64_t>(5 * 1024 * 1024)); |
| } |
| |
| void MemoryPeakDetector::TearDown() { |
| if (task_runner_) { |
| task_runner_->PostTask( |
| FROM_HERE, |
| BindOnce(&MemoryPeakDetector::TearDownInternal, Unretained(this))); |
| } |
| task_runner_ = nullptr; |
| } |
| |
| void MemoryPeakDetector::Start(MemoryPeakDetector::Config config) { |
| if (!config.polling_interval_ms) { |
| NOTREACHED(); |
| return; |
| } |
| task_runner_->PostTask(FROM_HERE, BindOnce(&MemoryPeakDetector::StartInternal, |
| Unretained(this), config)); |
| } |
| |
| void MemoryPeakDetector::Stop() { |
| task_runner_->PostTask( |
| FROM_HERE, BindOnce(&MemoryPeakDetector::StopInternal, Unretained(this))); |
| } |
| |
| void MemoryPeakDetector::Throttle() { |
| if (!task_runner_) |
| return; // Can be called before Setup(). |
| task_runner_->PostTask( |
| FROM_HERE, BindOnce(&MemoryPeakDetector::ResetPollHistory, |
| Unretained(this), true /* keep_last_sample */)); |
| } |
| |
| void MemoryPeakDetector::NotifyMemoryDumpProvidersChanged() { |
| if (!task_runner_) |
| return; // Can be called before Setup(). |
| task_runner_->PostTask( |
| FROM_HERE, |
| BindOnce(&MemoryPeakDetector::ReloadDumpProvidersAndStartPollingIfNeeded, |
| Unretained(this))); |
| } |
| |
| void MemoryPeakDetector::StartInternal(MemoryPeakDetector::Config config) { |
| DCHECK_EQ(DISABLED, state_); |
| state_ = ENABLED; |
| config_ = config; |
| ResetPollHistory(); |
| |
| // If there are any dump providers available, |
| // NotifyMemoryDumpProvidersChanged will fetch them and start the polling. |
| // Otherwise this will remain in the ENABLED state and the actual polling |
| // will start on the next call to |
| // ReloadDumpProvidersAndStartPollingIfNeeded(). |
| // Depending on the sandbox model, it is possible that no polling-capable |
| // dump providers will be ever available. |
| ReloadDumpProvidersAndStartPollingIfNeeded(); |
| } |
| |
| void MemoryPeakDetector::StopInternal() { |
| DCHECK_NE(NOT_INITIALIZED, state_); |
| state_ = DISABLED; |
| ++generation_; |
| for (const scoped_refptr<MemoryDumpProviderInfo>& mdp_info : dump_providers_) |
| mdp_info->dump_provider->SuspendFastMemoryPolling(); |
| dump_providers_.clear(); |
| } |
| |
| void MemoryPeakDetector::TearDownInternal() { |
| StopInternal(); |
| get_dump_providers_function_.Reset(); |
| on_peak_detected_callback_.Reset(); |
| state_ = NOT_INITIALIZED; |
| } |
| |
| void MemoryPeakDetector::ReloadDumpProvidersAndStartPollingIfNeeded() { |
| if (state_ == DISABLED || state_ == NOT_INITIALIZED) |
| return; // Start() will re-fetch the MDP list later. |
| |
| DCHECK((state_ == RUNNING && !dump_providers_.empty()) || |
| (state_ == ENABLED && dump_providers_.empty())); |
| |
| dump_providers_.clear(); |
| |
| // This is really MemoryDumpManager::GetDumpProvidersForPolling, % testing. |
| get_dump_providers_function_.Run(&dump_providers_); |
| |
| if (state_ == ENABLED && !dump_providers_.empty()) { |
| // It's now time to start polling for realz. |
| state_ = RUNNING; |
| task_runner_->PostTask( |
| FROM_HERE, BindOnce(&MemoryPeakDetector::PollMemoryAndDetectPeak, |
| Unretained(this), ++generation_)); |
| } else if (state_ == RUNNING && dump_providers_.empty()) { |
| // Will cause the next PollMemoryAndDetectPeak() task to early return. |
| state_ = ENABLED; |
| ++generation_; |
| } |
| } |
| |
| void MemoryPeakDetector::PollMemoryAndDetectPeak(uint32_t expected_generation) { |
| if (state_ != RUNNING || generation_ != expected_generation) |
| return; |
| |
| // We should never end up in a situation where state_ == RUNNING but all dump |
| // providers are gone. |
| DCHECK(!dump_providers_.empty()); |
| |
| poll_tasks_count_for_testing_++; |
| uint64_t polled_mem_bytes = 0; |
| for (const scoped_refptr<MemoryDumpProviderInfo>& mdp_info : |
| dump_providers_) { |
| DCHECK(mdp_info->options.is_fast_polling_supported); |
| uint64_t value = 0; |
| mdp_info->dump_provider->PollFastMemoryTotal(&value); |
| polled_mem_bytes += value; |
| } |
| if (config_.enable_verbose_poll_tracing) { |
| TRACE_COUNTER1(MemoryDumpManager::kTraceCategory, "PolledMemoryMB", |
| polled_mem_bytes / 1024 / 1024); |
| } |
| |
| // Peak detection logic. Design doc: https://goo.gl/0kOU4A . |
| bool is_peak = false; |
| if (skip_polls_ > 0) { |
| skip_polls_--; |
| } else if (last_dump_memory_total_ == 0) { |
| last_dump_memory_total_ = polled_mem_bytes; |
| } else if (polled_mem_bytes > 0) { |
| int64_t diff_from_last_dump = polled_mem_bytes - last_dump_memory_total_; |
| |
| DCHECK_GT(static_threshold_bytes_, 0u); |
| is_peak = |
| diff_from_last_dump > static_cast<int64_t>(static_threshold_bytes_); |
| |
| if (!is_peak) |
| is_peak = DetectPeakUsingSlidingWindowStddev(polled_mem_bytes); |
| } |
| |
| DCHECK_GT(config_.polling_interval_ms, 0u); |
| SequencedTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| BindOnce(&MemoryPeakDetector::PollMemoryAndDetectPeak, Unretained(this), |
| expected_generation), |
| TimeDelta::FromMilliseconds(config_.polling_interval_ms)); |
| |
| if (!is_peak) |
| return; |
| TRACE_EVENT_INSTANT1(MemoryDumpManager::kTraceCategory, |
| "Peak memory detected", TRACE_EVENT_SCOPE_PROCESS, |
| "PolledMemoryMB", polled_mem_bytes / 1024 / 1024); |
| ResetPollHistory(true /* keep_last_sample */); |
| last_dump_memory_total_ = polled_mem_bytes; |
| on_peak_detected_callback_.Run(); |
| } |
| |
| bool MemoryPeakDetector::DetectPeakUsingSlidingWindowStddev( |
| uint64_t polled_mem_bytes) { |
| DCHECK(polled_mem_bytes); |
| samples_bytes_[samples_index_] = polled_mem_bytes; |
| samples_index_ = (samples_index_ + 1) % kSlidingWindowNumSamples; |
| float mean = 0; |
| for (uint32_t i = 0; i < kSlidingWindowNumSamples; ++i) { |
| if (samples_bytes_[i] == 0) |
| return false; // Not enough samples to detect peaks. |
| mean += samples_bytes_[i]; |
| } |
| mean /= kSlidingWindowNumSamples; |
| float variance = 0; |
| for (uint32_t i = 0; i < kSlidingWindowNumSamples; ++i) { |
| const float deviation = samples_bytes_[i] - mean; |
| variance += deviation * deviation; |
| } |
| variance /= kSlidingWindowNumSamples; |
| |
| // If stddev is less than 0.2% then we consider that the process is inactive. |
| if (variance < (mean / 500) * (mean / 500)) |
| return false; |
| |
| // (mean + 3.69 * stddev) corresponds to a value that is higher than current |
| // sample with 99.99% probability. |
| const float cur_sample_deviation = polled_mem_bytes - mean; |
| return cur_sample_deviation * cur_sample_deviation > (3.69 * 3.69 * variance); |
| } |
| |
| void MemoryPeakDetector::ResetPollHistory(bool keep_last_sample) { |
| // TODO(primiano,ssid): this logic should probably be revisited. In the case |
| // of Android, the browser process sees the total of all processes memory in |
| // the same peak detector instance. Perhaps the best thing to do here is to |
| // keep the window of samples around and just bump the skip_polls_. |
| last_dump_memory_total_ = 0; |
| if (keep_last_sample) { |
| const uint32_t prev_index = |
| samples_index_ > 0 ? samples_index_ - 1 : kSlidingWindowNumSamples - 1; |
| last_dump_memory_total_ = samples_bytes_[prev_index]; |
| } |
| memset(samples_bytes_, 0, sizeof(samples_bytes_)); |
| samples_index_ = 0; |
| skip_polls_ = 0; |
| if (config_.polling_interval_ms > 0) { |
| skip_polls_ = |
| (config_.min_time_between_peaks_ms + config_.polling_interval_ms - 1) / |
| config_.polling_interval_ms; |
| } |
| } |
| |
| void MemoryPeakDetector::SetStaticThresholdForTesting( |
| uint64_t static_threshold_bytes) { |
| DCHECK_EQ(DISABLED, state_); |
| static_threshold_bytes_ = static_threshold_bytes; |
| } |
| |
| MemoryPeakDetector::MemoryPeakDetector::Config::Config() |
| : Config(0, 0, false) {} |
| |
| MemoryPeakDetector::MemoryPeakDetector::Config::Config( |
| uint32_t polling_interval_ms, |
| uint32_t min_time_between_peaks_ms, |
| bool enable_verbose_poll_tracing) |
| : polling_interval_ms(polling_interval_ms), |
| min_time_between_peaks_ms(min_time_between_peaks_ms), |
| enable_verbose_poll_tracing(enable_verbose_poll_tracing) {} |
| |
| } // namespace trace_event |
| } // namespace base |