// 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 #include #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 { 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; 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* 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(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 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 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* 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 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, size_t time_index, int delay_ms) { // Don't block shutdown on these tasks, as UMA will be disappearing. scoped_refptr 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