// Copyright (c) 2012 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/boot_times_loader.h" #include #include "base/bind.h" #include "base/command_line.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/location.h" #include "base/message_loop/message_loop.h" #include "base/message_loop/message_loop_proxy.h" #include "base/metrics/histogram.h" #include "base/prefs/pref_service.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/threading/thread.h" #include "base/threading/thread_restrictions.h" #include "base/time/time.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_iterator.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/pref_names.h" #include "components/user_manager/user_manager.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" using content::BrowserThread; using content::NavigationController; using content::RenderWidgetHost; using content::RenderWidgetHostView; using content::WebContents; namespace { const char kUptime[] = "uptime"; const char kDisk[] = "disk"; RenderWidgetHost* GetRenderWidgetHost(NavigationController* tab) { WebContents* web_contents = tab->GetWebContents(); if (web_contents) { RenderWidgetHostView* render_widget_host_view = web_contents->GetRenderWidgetHostView(); if (render_widget_host_view) return render_widget_host_view->GetRenderWidgetHost(); } return NULL; } const std::string GetTabUrl(RenderWidgetHost* rwh) { RenderWidgetHostView* rwhv = rwh->GetView(); for (chrome::BrowserIterator it; !it.done(); it.Next()) { Browser* browser = *it; for (int i = 0, tab_count = browser->tab_strip_model()->count(); i < tab_count; ++i) { WebContents* tab = browser->tab_strip_model()->GetWebContentsAt(i); if (tab->GetRenderWidgetHostView() == rwhv) { return tab->GetLastCommittedURL().spec(); } } } return std::string(); } // Appends the given buffer into the file. Returns the number of bytes // written, or -1 on error. // TODO(satorux): Move this to file_util. int AppendFile(const base::FilePath& file_path, const char* data, int size) { FILE* file = base::OpenFile(file_path, "a"); if (!file) return -1; const int num_bytes_written = fwrite(data, 1, size, file); base::CloseFile(file); return num_bytes_written; } } // namespace namespace chromeos { #define FPL(value) FILE_PATH_LITERAL(value) // Dir uptime & disk logs are located in. static const base::FilePath::CharType kLogPath[] = FPL("/tmp"); // Dir log{in,out} logs are located in. static const base::FilePath::CharType kLoginLogPath[] = FPL("/home/chronos/user"); // Prefix for the time measurement files. static const base::FilePath::CharType kUptimePrefix[] = FPL("uptime-"); // Prefix for the disk usage files. static const base::FilePath::CharType kDiskPrefix[] = FPL("disk-"); // Name of the time that Chrome's main() is called. static const base::FilePath::CharType kChromeMain[] = FPL("chrome-main"); // Delay in milliseconds before writing the login times to disk. static const int64 kLoginTimeWriteDelayMs = 3000; // Names of login stats files. static const base::FilePath::CharType kLoginSuccess[] = FPL("login-success"); static const base::FilePath::CharType kChromeFirstRender[] = FPL("chrome-first-render"); // Names of login UMA values. static const char kUmaLogin[] = "BootTime.Login"; static const char kUmaLoginNewUser[] = "BootTime.LoginNewUser"; static const char kUmaLoginPrefix[] = "BootTime."; static const char kUmaLogout[] = "ShutdownTime.Logout"; static const char kUmaLogoutPrefix[] = "ShutdownTime."; static const char kUmaRestart[] = "ShutdownTime.Restart"; // Name of file collecting login times. static const base::FilePath::CharType kLoginTimes[] = FPL("login-times"); // Name of file collecting logout times. static const char kLogoutTimes[] = "logout-times"; static base::LazyInstance g_boot_times_loader = LAZY_INSTANCE_INITIALIZER; // static BootTimesLoader::Stats BootTimesLoader::Stats::GetCurrentStats() { const base::FilePath kProcUptime(FPL("/proc/uptime")); const base::FilePath kDiskStat(FPL("/sys/block/sda/stat")); Stats stats; // Callers of this method expect synchronous behavior. // It's safe to allow IO here, because only virtual FS are accessed. base::ThreadRestrictions::ScopedAllowIO allow_io; base::ReadFileToString(kProcUptime, &stats.uptime_); base::ReadFileToString(kDiskStat, &stats.disk_); return stats; } std::string BootTimesLoader::Stats::SerializeToString() const { if (uptime_.empty() && disk_.empty()) return std::string(); base::DictionaryValue dictionary; dictionary.SetString(kUptime, uptime_); dictionary.SetString(kDisk, disk_); std::string result; if (!base::JSONWriter::Write(&dictionary, &result)) { LOG(WARNING) << "BootTimesLoader::Stats::SerializeToString(): failed."; return std::string(); } return result; } // static BootTimesLoader::Stats BootTimesLoader::Stats::DeserializeFromString( const std::string& source) { if (source.empty()) return Stats(); scoped_ptr value(base::JSONReader::Read(source)); base::DictionaryValue* dictionary; if (!value || !value->GetAsDictionary(&dictionary)) { LOG(ERROR) << "BootTimesLoader::Stats::DeserializeFromString(): not a " "dictionary: '" << source << "'"; return Stats(); } Stats result; if (!dictionary->GetString(kUptime, &result.uptime_) || !dictionary->GetString(kDisk, &result.disk_)) { LOG(ERROR) << "BootTimesLoader::Stats::DeserializeFromString(): format error: '" << source << "'"; return Stats(); } return result; } bool BootTimesLoader::Stats::UptimeDouble(double* result) const { std::string uptime = uptime_; const size_t space_at = uptime.find_first_of(' '); if (space_at == std::string::npos) return false; uptime.resize(space_at); if (base::StringToDouble(uptime, result)) return true; return false; } void BootTimesLoader::Stats::RecordStats(const std::string& name) const { BrowserThread::PostBlockingPoolTask( FROM_HERE, base::Bind(&BootTimesLoader::Stats::RecordStatsImpl, base::Owned(new Stats(*this)), name)); } void BootTimesLoader::Stats::RecordStatsWithCallback( const std::string& name, const base::Closure& callback) const { BrowserThread::PostBlockingPoolTaskAndReply( FROM_HERE, base::Bind(&BootTimesLoader::Stats::RecordStatsImpl, base::Owned(new Stats(*this)), name), callback); } void BootTimesLoader::Stats::RecordStatsImpl( const base::FilePath::StringType& name) const { DCHECK(content::BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread()); const base::FilePath log_path(kLogPath); const base::FilePath uptime_output = log_path.Append(base::FilePath(kUptimePrefix + name)); const base::FilePath disk_output = log_path.Append(base::FilePath(kDiskPrefix + name)); // Append numbers to the files. AppendFile(uptime_output, uptime_.data(), uptime_.size()); AppendFile(disk_output, disk_.data(), disk_.size()); } BootTimesLoader::BootTimesLoader() : backend_(new Backend()), have_registered_(false), login_done_(false), restart_requested_(false) { login_time_markers_.reserve(30); logout_time_markers_.reserve(30); } BootTimesLoader::~BootTimesLoader() { } // static BootTimesLoader* BootTimesLoader::Get() { return g_boot_times_loader.Pointer(); } // static void BootTimesLoader::WriteTimes( const std::string base_name, const std::string uma_name, const std::string uma_prefix, std::vector login_times) { const int kMinTimeMillis = 1; const int kMaxTimeMillis = 30000; const int kNumBuckets = 100; const base::FilePath log_path(kLoginLogPath); // Need to sort by time since the entries may have been pushed onto the // vector (on the UI thread) in a different order from which they were // created (potentially on other threads). std::sort(login_times.begin(), login_times.end()); base::Time first = login_times.front().time(); base::Time last = login_times.back().time(); base::TimeDelta total = last - first; base::HistogramBase* total_hist = base::Histogram::FactoryTimeGet( uma_name, base::TimeDelta::FromMilliseconds(kMinTimeMillis), base::TimeDelta::FromMilliseconds(kMaxTimeMillis), kNumBuckets, base::HistogramBase::kUmaTargetedHistogramFlag); total_hist->AddTime(total); std::string output = base::StringPrintf("%s: %.2f", uma_name.c_str(), total.InSecondsF()); base::Time prev = first; for (unsigned int i = 0; i < login_times.size(); ++i) { TimeMarker tm = login_times[i]; base::TimeDelta since_first = tm.time() - first; base::TimeDelta since_prev = tm.time() - prev; std::string name; if (tm.send_to_uma()) { name = uma_prefix + tm.name(); base::HistogramBase* prev_hist = base::Histogram::FactoryTimeGet( name, base::TimeDelta::FromMilliseconds(kMinTimeMillis), base::TimeDelta::FromMilliseconds(kMaxTimeMillis), kNumBuckets, base::HistogramBase::kUmaTargetedHistogramFlag); prev_hist->AddTime(since_prev); } else { name = tm.name(); } output += base::StringPrintf( "\n%.2f +%.4f %s", since_first.InSecondsF(), since_prev.InSecondsF(), name.data()); prev = tm.time(); } output += '\n'; base::WriteFile(log_path.Append(base_name), output.data(), output.size()); } void BootTimesLoader::LoginDone(bool is_user_new) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (login_done_) return; login_done_ = true; AddLoginTimeMarker("LoginDone", false); RecordCurrentStats(kChromeFirstRender); if (have_registered_) { registrar_.Remove(this, content::NOTIFICATION_LOAD_START, content::NotificationService::AllSources()); registrar_.Remove(this, content::NOTIFICATION_LOAD_STOP, content::NotificationService::AllSources()); registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, content::NotificationService::AllSources()); registrar_.Remove( this, content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, content::NotificationService::AllSources()); } // Don't swamp the FILE thread right away. BrowserThread::PostDelayedTask( BrowserThread::FILE, FROM_HERE, base::Bind(&WriteTimes, kLoginTimes, (is_user_new ? kUmaLoginNewUser : kUmaLogin), kUmaLoginPrefix, login_time_markers_), base::TimeDelta::FromMilliseconds(kLoginTimeWriteDelayMs)); } void BootTimesLoader::WriteLogoutTimes() { // Either we're on the browser thread, or (more likely) Chrome is in the // process of shutting down and we're on the main thread but the message loop // has already been terminated. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) || !BrowserThread::IsMessageLoopValid(BrowserThread::UI)); WriteTimes(kLogoutTimes, (restart_requested_ ? kUmaRestart : kUmaLogout), kUmaLogoutPrefix, logout_time_markers_); } // static void BootTimesLoader::ClearLogoutStartedLastPreference() { PrefService* local_state = g_browser_process->local_state(); local_state->ClearPref(prefs::kLogoutStartedLast); } void BootTimesLoader::OnChromeProcessStart() { PrefService* local_state = g_browser_process->local_state(); const std::string logout_started_last_str = local_state->GetString(prefs::kLogoutStartedLast); if (logout_started_last_str.empty()) return; // Note that kLogoutStartedLast is not cleared on format error to stay in // logs in case of other fatal system errors. const Stats logout_started_last_stats = Stats::DeserializeFromString(logout_started_last_str); if (logout_started_last_stats.uptime().empty()) return; double logout_started_last; double uptime; if (!logout_started_last_stats.UptimeDouble(&logout_started_last) || !Stats::GetCurrentStats().UptimeDouble(&uptime)) { return; } if (logout_started_last >= uptime) { // Reboot happened. ClearLogoutStartedLastPreference(); return; } // Write /tmp/uptime-logout-started as well. const char kLogoutStarted[] = "logout-started"; logout_started_last_stats.RecordStatsWithCallback( kLogoutStarted, base::Bind(&BootTimesLoader::ClearLogoutStartedLastPreference)); } void BootTimesLoader::OnLogoutStarted(PrefService* state) { const std::string uptime = Stats::GetCurrentStats().SerializeToString(); if (!uptime.empty()) state->SetString(prefs::kLogoutStartedLast, uptime); } void BootTimesLoader::RecordCurrentStats(const std::string& name) { Stats::GetCurrentStats().RecordStats(name); } void BootTimesLoader::SaveChromeMainStats() { chrome_main_stats_ = Stats::GetCurrentStats(); } void BootTimesLoader::RecordChromeMainStats() { chrome_main_stats_.RecordStats(kChromeMain); } void BootTimesLoader::RecordLoginAttempted() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (login_done_) return; login_time_markers_.clear(); AddLoginTimeMarker("LoginStarted", false); if (!have_registered_) { have_registered_ = true; registrar_.Add(this, content::NOTIFICATION_LOAD_START, content::NotificationService::AllSources()); registrar_.Add(this, content::NOTIFICATION_LOAD_STOP, content::NotificationService::AllSources()); registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, content::NotificationService::AllSources()); registrar_.Add( this, content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, content::NotificationService::AllSources()); } } void BootTimesLoader::AddLoginTimeMarker( const std::string& marker_name, bool send_to_uma) { AddMarker(&login_time_markers_, TimeMarker(marker_name, send_to_uma)); } void BootTimesLoader::AddLogoutTimeMarker( const std::string& marker_name, bool send_to_uma) { AddMarker(&logout_time_markers_, TimeMarker(marker_name, send_to_uma)); } // static void BootTimesLoader::AddMarker(std::vector* vector, TimeMarker marker) { // The marker vectors can only be safely manipulated on the main thread. // If we're late in the process of shutting down (eg. as can be the case at // logout), then we have to assume we're on the main thread already. if (BrowserThread::CurrentlyOn(BrowserThread::UI) || !BrowserThread::IsMessageLoopValid(BrowserThread::UI)) { vector->push_back(marker); } else { // Add the marker on the UI thread. // Note that it's safe to use an unretained pointer to the vector because // BootTimesLoader's lifetime exceeds that of the UI thread message loop. BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&BootTimesLoader::AddMarker, base::Unretained(vector), marker)); } } void BootTimesLoader::RecordAuthenticationSuccess() { AddLoginTimeMarker("Authenticate", true); RecordCurrentStats(kLoginSuccess); } void BootTimesLoader::RecordAuthenticationFailure() { // Do nothing for now. } void BootTimesLoader::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case content::NOTIFICATION_LOAD_START: { NavigationController* tab = content::Source(source).ptr(); RenderWidgetHost* rwh = GetRenderWidgetHost(tab); DCHECK(rwh); AddLoginTimeMarker("TabLoad-Start: " + GetTabUrl(rwh), false); render_widget_hosts_loading_.insert(rwh); break; } case content::NOTIFICATION_LOAD_STOP: { NavigationController* tab = content::Source(source).ptr(); RenderWidgetHost* rwh = GetRenderWidgetHost(tab); if (render_widget_hosts_loading_.find(rwh) != render_widget_hosts_loading_.end()) { AddLoginTimeMarker("TabLoad-End: " + GetTabUrl(rwh), false); } break; } case content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE: { RenderWidgetHost* rwh = content::Source(source).ptr(); if (render_widget_hosts_loading_.find(rwh) != render_widget_hosts_loading_.end()) { AddLoginTimeMarker("TabPaint: " + GetTabUrl(rwh), false); LoginDone(user_manager::UserManager::Get()->IsCurrentUserNew()); } break; } case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: { WebContents* web_contents = content::Source(source).ptr(); RenderWidgetHost* render_widget_host = GetRenderWidgetHost(&web_contents->GetController()); render_widget_hosts_loading_.erase(render_widget_host); break; } default: break; } } } // namespace chromeos