// 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 #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& 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(SysInfo::AmountOfPhysicalMemory()) / 100; #endif // Fallback, mostly for test environments where AmountOfPhysicalMemory() is // broken. static_threshold_bytes_ = std::max(static_threshold_bytes_, static_cast(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& 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& 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(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