| // Copyright 2013 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 "chrome/browser/chromeos/swap_metrics.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "ash/shell.h" |
| #include "base/file_util.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/cancellation_flag.h" |
| #include "base/sys_info.h" |
| #include "base/threading/sequenced_worker_pool.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "ui/base/events/event.h" |
| |
| using base::FilePath; |
| |
| namespace chromeos { |
| namespace { |
| |
| // Time delays for metrics collections, starting after an interesting UI event. |
| // Times are relative to the UI event. Start with zero to record the initial |
| // state of the metrics immediately. |
| const int kMetricsDelayMs[] = { 0, 100, 300, 1000, 3000 }; |
| |
| } // namespace |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| // Runs in the blocking thread pool to load metrics and record them. |
| // Reads data about CPU utilization and swap activity from the /proc and /sys |
| // file systems. Owned by SwapMetrics on the UI thread. |
| class SwapMetrics::Backend : public base::RefCountedThreadSafe<Backend> { |
| public: |
| explicit Backend(const std::string& reason); |
| |
| // Records one set of statistics for |time_index| after the interesting |
| // event. May trigger another delayed task to record more statistics. |
| void RecordMetricsOnBlockingPool(size_t time_index); |
| |
| // Sets the thread-safe cancellation flag. |
| void CancelOnUIThread() { cancelled_.Set(); } |
| |
| private: |
| friend class base::RefCountedThreadSafe<Backend>; |
| |
| virtual ~Backend(); |
| |
| // Extracts a field value from a list of name-value pairs |
| // in a file (typically a /proc or /sys file). Returns false |
| // if the field is not found, or for other errors. |
| bool GetFieldFromKernelOutput(const std::string& path, |
| const std::string& field, |
| int64* value); |
| |
| // Reads a file whose content is a single line, and returns its content as a |
| // list of tokens. |expected_tokens_count| is the expected number of tokens. |
| // |delimiters| is a string containing characters that may appear between |
| // tokens. Returns true on success, false otherwise. |
| bool TokenizeOneLineFile(const std::string& path, |
| size_t expected_tokens_count, |
| const std::string& delimiters, |
| std::vector<std::string>* tokens); |
| |
| // Retrieve various system metrics. Return true on success. |
| bool GetMeminfoField(const std::string& field, int64* value); |
| bool GetUptime(double* uptime_secs, double* idle_time_secs); |
| bool GetPageFaults(int64* page_faults); |
| |
| // Record histogram samples. |
| void RecordFaultsHistogramSample(int faults, |
| const std::string& reason, |
| int swap_group, |
| int time_index); |
| void RecordCpuHistogramSample(int cpu, |
| const std::string& reason, |
| int swap_group, |
| int time_index); |
| |
| // Cancellation flag that can be written from the UI thread (which owns this |
| // object) and read from any thread. |
| base::CancellationFlag cancelled_; |
| |
| // Data initialized once and shared by all instances of this class. |
| static bool first_time_; |
| static int64 swap_total_kb_; |
| static int number_of_cpus_; |
| |
| // Values at the beginning of each sampling interval. |
| double last_uptime_secs_; |
| double last_idle_time_secs_; |
| int64 last_page_faults_; |
| |
| // Swap group for a set of samples, chosen at the beginning of the set. |
| int swap_group_; |
| |
| // Reason for sampling. |
| const std::string reason_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Backend); |
| }; |
| |
| // static |
| bool SwapMetrics::Backend::first_time_ = true; |
| // static |
| int64 SwapMetrics::Backend::swap_total_kb_ = 0; |
| // static |
| int SwapMetrics::Backend::number_of_cpus_ = 0; |
| |
| SwapMetrics::Backend::Backend(const std::string& reason) |
| : last_uptime_secs_(0.0), |
| last_idle_time_secs_(0.0), |
| last_page_faults_(0), |
| swap_group_(0), |
| reason_(reason) { |
| } |
| |
| SwapMetrics::Backend::~Backend() { |
| } |
| |
| void SwapMetrics::Backend::RecordMetricsOnBlockingPool(size_t time_index) { |
| DCHECK(content::BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread()); |
| // Another UI event might have occurred. Don't run this metrics collection |
| // and don't post another task. This object's refcount will drop and it will |
| // be deleted when the next backend is created on the UI thread. |
| if (cancelled_.IsSet()) |
| return; |
| DVLOG(1) << "RecordMetricsInBlockingPool " << time_index; |
| |
| // At init time, get the number of cpus and the total swap. The number of cpus |
| // is necessary because the idle time reported is the sum of the idle |
| // times of all cpus. |
| // |
| // When an event occurs: |
| // - At time t_0, save the initial values for uptime, idle_time, page_faults, |
| // and swap used. |
| // |
| // - At time t_i, compute cpu utilization as a fraction (1 = fully utilized): |
| // utilization = |
| // 1 - (idle_time_i - idle_time_0) / (uptime_i - uptime_0) / ncpus |
| // |
| // then UMA-report it in the right swap group. Do the same for page faults. |
| |
| if (first_time_) { |
| first_time_ = false; |
| number_of_cpus_ = base::SysInfo::NumberOfProcessors(); |
| // Avoid divide by zero in case of errors. |
| if (number_of_cpus_ == 0) |
| number_of_cpus_ = 1; |
| GetMeminfoField("SwapTotal:", &swap_total_kb_); |
| } |
| |
| if (time_index == 0) { |
| // Record baseline data. |
| GetUptime(&last_uptime_secs_, &last_idle_time_secs_); |
| GetPageFaults(&last_page_faults_); |
| int64 swap_free_kb = 0; |
| GetMeminfoField("SwapFree:", &swap_free_kb); |
| int swap_percent = swap_total_kb_ > 0 |
| ? (swap_total_kb_ - swap_free_kb) * 100 / swap_total_kb_ |
| : 0; |
| if (swap_percent < 10) |
| swap_group_ = 0; |
| else if (swap_percent < 30) |
| swap_group_ = 1; |
| else if (swap_percent < 60) |
| swap_group_ = 2; |
| else |
| swap_group_ = 3; |
| } else { |
| int64 page_faults = 0; |
| double idle_time_secs = 0.0; |
| double uptime_secs = 0.0; |
| GetUptime(&uptime_secs, &idle_time_secs); |
| GetPageFaults(&page_faults); |
| double delta_time_secs = uptime_secs - last_uptime_secs_; |
| // Unexpected, but not worth agonizing over it. |
| if (delta_time_secs == 0) |
| return; |
| |
| int cpu = (1.0 - (idle_time_secs - last_idle_time_secs_) / |
| delta_time_secs / number_of_cpus_) * 100; |
| int faults_per_sec = (page_faults - last_page_faults_) / delta_time_secs; |
| |
| RecordCpuHistogramSample(cpu, reason_, swap_group_, time_index); |
| RecordFaultsHistogramSample(faults_per_sec, reason_, |
| swap_group_, time_index); |
| |
| last_uptime_secs_ = uptime_secs; |
| last_page_faults_ = page_faults; |
| last_idle_time_secs_ = idle_time_secs; |
| } |
| |
| // Check if another metrics recording is needed. |
| if (++time_index >= arraysize(kMetricsDelayMs)) |
| return; |
| PostTaskRecordMetrics( |
| scoped_refptr<Backend>(this), |
| time_index, |
| kMetricsDelayMs[time_index] - kMetricsDelayMs[time_index - 1]); |
| } |
| |
| void SwapMetrics::Backend::RecordFaultsHistogramSample( |
| int faults, |
| const std::string& reason, |
| int swap_group, |
| int time_index) { |
| std::string name = |
| base::StringPrintf("Platform.SwapJank.%s.Faults.Swap%d.Time%d", |
| reason.c_str(), |
| swap_group, |
| time_index); |
| const int kMinimumBucket = 10; |
| const int kMaximumBucket = 200000; |
| const size_t kBucketCount = 50; |
| base::HistogramBase* counter = |
| base::Histogram::FactoryGet(name, |
| kMinimumBucket, |
| kMaximumBucket, |
| kBucketCount, |
| base::Histogram::kUmaTargetedHistogramFlag); |
| counter->Add(faults); |
| } |
| |
| void SwapMetrics::Backend::RecordCpuHistogramSample(int cpu, |
| const std::string& reason, |
| int swap_group, |
| int time_index) { |
| std::string name = |
| base::StringPrintf("Platform.SwapJank.%s.Cpu.Swap%d.Time%d", |
| reason.c_str(), |
| swap_group, |
| time_index); |
| const int kMinimumBucket = 0; |
| const int kMaximumBucket = 101; |
| const size_t kBucketCount = 102; |
| base::HistogramBase* counter = base::LinearHistogram::FactoryGet( |
| name, |
| kMinimumBucket, |
| kMaximumBucket, |
| kBucketCount, |
| base::Histogram::kUmaTargetedHistogramFlag); |
| counter->Add(cpu); |
| } |
| |
| // Extracts a field value from a list of name-value pairs |
| // in a file (typically a /proc or /sys file). Returns false |
| // if the field is not found, or for other errors. |
| bool SwapMetrics::Backend::GetFieldFromKernelOutput(const std::string& path, |
| const std::string& field, |
| int64* value) { |
| std::string file_content; |
| if (!file_util::ReadFileToString(FilePath(path), &file_content)) { |
| LOG(WARNING) << "Cannot read " << path; |
| return false; |
| } |
| std::vector<std::string> lines; |
| size_t line_count = Tokenize(file_content, "\n", &lines); |
| if (line_count < 2) { |
| LOG(WARNING) << "Error breaking " << path << " into lines"; |
| return false; |
| } |
| for (size_t i = 0; i < line_count; ++i) { |
| std::vector<std::string> tokens; |
| size_t token_count = Tokenize(lines[i], " ", &tokens); |
| if (token_count < 2) { |
| LOG(WARNING) << "Unexpected line: " << lines[i]; |
| return false; |
| } |
| if (tokens[0].compare(field) != 0) |
| continue; |
| if (!base::StringToInt64(tokens[1], value)) { |
| LOG(WARNING) << "Cannot convert " << tokens[1] << " to int"; |
| return false; |
| } |
| return true; |
| } |
| LOG(WARNING) << "could not find field " << field; |
| return false; |
| } |
| |
| bool SwapMetrics::Backend::TokenizeOneLineFile(const std::string& path, |
| size_t expected_tokens_count, |
| const std::string& delimiters, |
| std::vector<std::string>* |
| tokens) { |
| std::string file_content; |
| if (!file_util::ReadFileToString(FilePath(path), &file_content)) { |
| LOG(WARNING) << "cannot read " << path; |
| return false; |
| } |
| size_t tokens_count = Tokenize(file_content, delimiters, tokens); |
| if (tokens_count != expected_tokens_count) { |
| LOG(WARNING) << "unexpected content of " << path << ": " << file_content; |
| return false; |
| } |
| return true; |
| } |
| |
| bool SwapMetrics::Backend::GetMeminfoField(const std::string& name, |
| int64* value) { |
| return GetFieldFromKernelOutput("/proc/meminfo", name, value); |
| } |
| |
| bool SwapMetrics::Backend::GetUptime(double* uptime_secs, |
| double* idle_time_secs) { |
| // Get the time since boot. |
| const char kUptimePath[] = "/proc/uptime"; |
| std::vector<std::string> tokens; |
| if (!TokenizeOneLineFile(kUptimePath, 2, " \n", &tokens)) |
| return false; |
| |
| if (!base::StringToDouble(tokens[0], uptime_secs)) { |
| LOG(WARNING) << "cannot convert " << tokens[0] << " to double"; |
| return false; |
| } |
| |
| // Get the idle time since boot. The number available in /proc/stat is more |
| // precise, but this one should be good enough. |
| if (!base::StringToDouble(tokens[1], idle_time_secs)) { |
| LOG(WARNING) << "cannot convert " << tokens[0] << " to double"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool SwapMetrics::Backend::GetPageFaults(int64* page_faults) { |
| // Get number of page faults. |
| return GetFieldFromKernelOutput("/proc/vmstat", "pgmajfault", page_faults); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| SwapMetrics::SwapMetrics() : browser_(NULL) { |
| ash::Shell::GetInstance()->AddPreTargetHandler(this); |
| BrowserList::AddObserver(this); |
| } |
| |
| SwapMetrics::~SwapMetrics() { |
| if (backend_.get()) |
| backend_->CancelOnUIThread(); |
| ash::Shell::GetInstance()->RemovePreTargetHandler(this); |
| BrowserList::RemoveObserver(this); |
| SetBrowser(NULL); |
| } |
| |
| void SwapMetrics::OnBrowserRemoved(Browser* browser) { |
| if (browser_ == browser) |
| SetBrowser(NULL); |
| } |
| |
| void SwapMetrics::OnBrowserSetLastActive(Browser* browser) { |
| if (browser && browser->type() == Browser::TYPE_TABBED) |
| SetBrowser(browser); |
| else |
| SetBrowser(NULL); |
| } |
| |
| void SwapMetrics::ActiveTabChanged(content::WebContents* old_contents, |
| content::WebContents* new_contents, |
| int index, |
| int reason) { |
| // Only measure tab switches, not tabs being replaced with new contents. |
| if (reason != TabStripModelObserver::CHANGE_REASON_USER_GESTURE) |
| return; |
| DVLOG(1) << "ActiveTabChanged"; |
| StartMetricsCollection("TabSwitch"); |
| } |
| |
| // This exists primarily for debugging on desktop builds. |
| void SwapMetrics::OnMouseEvent(ui::MouseEvent* event) { |
| if (event->type() != ui::ET_MOUSEWHEEL) |
| return; |
| DVLOG(1) << "OnMouseEvent"; |
| StartMetricsCollection("Scroll"); |
| } |
| |
| void SwapMetrics::OnScrollEvent(ui::ScrollEvent* event) { |
| if (event->type() != ui::ET_SCROLL && |
| event->type() != ui::ET_SCROLL_FLING_START) |
| return; |
| DVLOG(1) << "OnScrollEvent"; |
| StartMetricsCollection("Scroll"); |
| } |
| |
| // static |
| void SwapMetrics::PostTaskRecordMetrics(scoped_refptr<Backend> backend, |
| size_t time_index, |
| int delay_ms) { |
| // Don't block shutdown on these tasks, as UMA will be disappearing. |
| scoped_refptr<base::TaskRunner> runner = |
| content::BrowserThread::GetBlockingPool()-> |
| GetTaskRunnerWithShutdownBehavior( |
| base::SequencedWorkerPool::SKIP_ON_SHUTDOWN); |
| runner->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&SwapMetrics::Backend::RecordMetricsOnBlockingPool, |
| backend, |
| time_index), |
| base::TimeDelta::FromMilliseconds(delay_ms)); |
| } |
| |
| void SwapMetrics::StartMetricsCollection(const std::string& reason) { |
| // Cancel any existing metrics run. |
| if (backend_.get()) |
| backend_->CancelOnUIThread(); |
| backend_ = new Backend(reason); |
| PostTaskRecordMetrics(backend_, 0, kMetricsDelayMs[0]); |
| } |
| |
| void SwapMetrics::SetBrowser(Browser* browser) { |
| if (browser_ == browser) |
| return; |
| if (browser_) |
| browser_->tab_strip_model()->RemoveObserver(this); |
| browser_ = browser; |
| if (browser_) |
| browser_->tab_strip_model()->AddObserver(this); |
| } |
| |
| } // namespace chromeos |