boot_times_loader.cc revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/chromeos/boot_times_loader.h" 6 7#include <vector> 8 9#include "base/bind.h" 10#include "base/command_line.h" 11#include "base/file_util.h" 12#include "base/files/file_path.h" 13#include "base/lazy_instance.h" 14#include "base/location.h" 15#include "base/message_loop.h" 16#include "base/message_loop/message_loop_proxy.h" 17#include "base/metrics/histogram.h" 18#include "base/process_util.h" 19#include "base/strings/string_number_conversions.h" 20#include "base/strings/string_util.h" 21#include "base/strings/stringprintf.h" 22#include "base/threading/thread.h" 23#include "base/threading/thread_restrictions.h" 24#include "base/time/time.h" 25#include "chrome/browser/browser_process.h" 26#include "chrome/browser/chrome_notification_types.h" 27#include "chrome/browser/chromeos/login/authentication_notification_details.h" 28#include "chrome/browser/ui/browser.h" 29#include "chrome/browser/ui/browser_iterator.h" 30#include "chrome/browser/ui/tabs/tab_strip_model.h" 31#include "chrome/common/chrome_switches.h" 32#include "content/public/browser/browser_thread.h" 33#include "content/public/browser/navigation_controller.h" 34#include "content/public/browser/notification_service.h" 35#include "content/public/browser/render_widget_host_view.h" 36#include "content/public/browser/web_contents.h" 37 38using content::BrowserThread; 39using content::NavigationController; 40using content::RenderWidgetHost; 41using content::RenderWidgetHostView; 42using content::WebContents; 43 44namespace { 45 46RenderWidgetHost* GetRenderWidgetHost(NavigationController* tab) { 47 WebContents* web_contents = tab->GetWebContents(); 48 if (web_contents) { 49 RenderWidgetHostView* render_widget_host_view = 50 web_contents->GetRenderWidgetHostView(); 51 if (render_widget_host_view) 52 return render_widget_host_view->GetRenderWidgetHost(); 53 } 54 return NULL; 55} 56 57const std::string GetTabUrl(RenderWidgetHost* rwh) { 58 RenderWidgetHostView* rwhv = rwh->GetView(); 59 for (chrome::BrowserIterator it; !it.done(); it.Next()) { 60 Browser* browser = *it; 61 for (int i = 0, tab_count = browser->tab_strip_model()->count(); 62 i < tab_count; 63 ++i) { 64 WebContents* tab = browser->tab_strip_model()->GetWebContentsAt(i); 65 if (tab->GetRenderWidgetHostView() == rwhv) { 66 return tab->GetURL().spec(); 67 } 68 } 69 } 70 return std::string(); 71} 72 73void PostCallbackIfNotCanceled( 74 const CancelableTaskTracker::IsCanceledCallback& is_canceled_cb, 75 base::TaskRunner* task_runner, 76 const chromeos::BootTimesLoader::GetBootTimesCallback& callback, 77 const chromeos::BootTimesLoader::BootTimes& boot_times) { 78 if (is_canceled_cb.Run()) 79 return; 80 task_runner->PostTask(FROM_HERE, base::Bind(callback, boot_times)); 81} 82 83} // namespace 84 85namespace chromeos { 86 87#define FPL(value) FILE_PATH_LITERAL(value) 88 89// Dir uptime & disk logs are located in. 90static const base::FilePath::CharType kLogPath[] = FPL("/tmp"); 91// Dir log{in,out} logs are located in. 92static const base::FilePath::CharType kLoginLogPath[] = 93 FPL("/home/chronos/user"); 94// Prefix for the time measurement files. 95static const base::FilePath::CharType kUptimePrefix[] = FPL("uptime-"); 96// Prefix for the disk usage files. 97static const base::FilePath::CharType kDiskPrefix[] = FPL("disk-"); 98// Name of the time that Chrome's main() is called. 99static const base::FilePath::CharType kChromeMain[] = FPL("chrome-main"); 100// Delay in milliseconds between file read attempts. 101static const int64 kReadAttemptDelayMs = 250; 102// Delay in milliseconds before writing the login times to disk. 103static const int64 kLoginTimeWriteDelayMs = 3000; 104 105// Names of login stats files. 106static const base::FilePath::CharType kLoginSuccess[] = FPL("login-success"); 107static const base::FilePath::CharType kChromeFirstRender[] = 108 FPL("chrome-first-render"); 109 110// Names of login UMA values. 111static const char kUmaLogin[] = "BootTime.Login"; 112static const char kUmaLoginPrefix[] = "BootTime."; 113static const char kUmaLogout[] = "ShutdownTime.Logout"; 114static const char kUmaLogoutPrefix[] = "ShutdownTime."; 115 116// Name of file collecting login times. 117static const base::FilePath::CharType kLoginTimes[] = FPL("login-times"); 118 119// Name of file collecting logout times. 120static const char kLogoutTimes[] = "logout-times"; 121 122static base::LazyInstance<BootTimesLoader> g_boot_times_loader = 123 LAZY_INSTANCE_INITIALIZER; 124 125BootTimesLoader::BootTimesLoader() 126 : backend_(new Backend()), 127 have_registered_(false) { 128 login_time_markers_.reserve(30); 129 logout_time_markers_.reserve(30); 130} 131 132BootTimesLoader::~BootTimesLoader() {} 133 134// static 135BootTimesLoader* BootTimesLoader::Get() { 136 return g_boot_times_loader.Pointer(); 137} 138 139CancelableTaskTracker::TaskId BootTimesLoader::GetBootTimes( 140 const GetBootTimesCallback& callback, 141 CancelableTaskTracker* tracker) { 142 if (!BrowserThread::IsMessageLoopValid(BrowserThread::FILE)) { 143 // This should only happen if Chrome is shutting down, so we don't do 144 // anything. 145 return CancelableTaskTracker::kBadTaskId; 146 } 147 148 const CommandLine& command_line = *CommandLine::ForCurrentProcess(); 149 if (command_line.HasSwitch(switches::kTestType)) { 150 // TODO(davemoore) This avoids boottimes for tests. This needs to be 151 // replaced with a mock of BootTimesLoader. 152 return CancelableTaskTracker::kBadTaskId; 153 } 154 155 CancelableTaskTracker::IsCanceledCallback is_canceled; 156 CancelableTaskTracker::TaskId id = tracker->NewTrackedTaskId(&is_canceled); 157 158 GetBootTimesCallback callback_runner = 159 base::Bind(&PostCallbackIfNotCanceled, 160 is_canceled, base::MessageLoopProxy::current(), callback); 161 BrowserThread::PostTask( 162 BrowserThread::FILE, 163 FROM_HERE, 164 base::Bind(&Backend::GetBootTimesAndRunCallback, 165 backend_, is_canceled, callback_runner)); 166 return id; 167} 168 169// Extracts the uptime value from files located in /tmp, returning the 170// value as a double in value. 171static bool GetTime(const base::FilePath::StringType& log, double* value) { 172 base::FilePath log_dir(kLogPath); 173 base::FilePath log_file = log_dir.Append(log); 174 std::string contents; 175 *value = 0.0; 176 if (file_util::ReadFileToString(log_file, &contents)) { 177 size_t space_index = contents.find(' '); 178 size_t chars_left = 179 space_index != std::string::npos ? space_index : std::string::npos; 180 std::string value_string = contents.substr(0, chars_left); 181 return base::StringToDouble(value_string, value); 182 } 183 return false; 184} 185 186// Converts double seconds to a TimeDelta object. 187static base::TimeDelta SecondsToTimeDelta(double seconds) { 188 double ms = seconds * base::Time::kMillisecondsPerSecond; 189 return base::TimeDelta::FromMilliseconds(static_cast<int64>(ms)); 190} 191 192// Reports the collected boot times to UMA if they haven't been 193// reported yet and if metrics collection is enabled. 194static void SendBootTimesToUMA(const BootTimesLoader::BootTimes& boot_times) { 195 // Checks if the times for the most recent boot event have been 196 // reported already to avoid sending boot time histogram samples 197 // every time the user logs out. 198 static const base::FilePath::CharType kBootTimesSent[] = 199 FPL("/tmp/boot-times-sent"); 200 base::FilePath sent(kBootTimesSent); 201 if (base::PathExists(sent)) 202 return; 203 204 UMA_HISTOGRAM_TIMES("BootTime.Total", 205 SecondsToTimeDelta(boot_times.total)); 206 UMA_HISTOGRAM_TIMES("BootTime.Firmware", 207 SecondsToTimeDelta(boot_times.firmware)); 208 UMA_HISTOGRAM_TIMES("BootTime.Kernel", 209 SecondsToTimeDelta(boot_times.pre_startup)); 210 UMA_HISTOGRAM_TIMES("BootTime.System", 211 SecondsToTimeDelta(boot_times.system)); 212 if (boot_times.chrome > 0) { 213 UMA_HISTOGRAM_TIMES("BootTime.Chrome", 214 SecondsToTimeDelta(boot_times.chrome)); 215 } 216 217 // Stores the boot times to a file in /tmp to indicate that the 218 // times for the most recent boot event have been reported 219 // already. The file will be deleted at system shutdown/reboot. 220 std::string boot_times_text = base::StringPrintf("total: %.2f\n" 221 "firmware: %.2f\n" 222 "kernel: %.2f\n" 223 "system: %.2f\n" 224 "chrome: %.2f\n", 225 boot_times.total, 226 boot_times.firmware, 227 boot_times.pre_startup, 228 boot_times.system, 229 boot_times.chrome); 230 file_util::WriteFile(sent, boot_times_text.data(), boot_times_text.size()); 231 DCHECK(base::PathExists(sent)); 232} 233 234void BootTimesLoader::Backend::GetBootTimesAndRunCallback( 235 const CancelableTaskTracker::IsCanceledCallback& is_canceled_cb, 236 const GetBootTimesCallback& callback) { 237 if (is_canceled_cb.Run()) 238 return; 239 240 const base::FilePath::CharType kFirmwareBootTime[] = 241 FPL("firmware-boot-time"); 242 const base::FilePath::CharType kPreStartup[] = FPL("pre-startup"); 243 const base::FilePath::CharType kChromeExec[] = FPL("chrome-exec"); 244 const base::FilePath::CharType kChromeMain[] = FPL("chrome-main"); 245 const base::FilePath::CharType kXStarted[] = FPL("x-started"); 246 const base::FilePath::CharType kLoginPromptReady[] = 247 FPL("login-prompt-ready"); 248 const base::FilePath::StringType uptime_prefix = kUptimePrefix; 249 250 // Wait until firmware-boot-time file exists by reposting. 251 base::FilePath log_dir(kLogPath); 252 base::FilePath log_file = log_dir.Append(kFirmwareBootTime); 253 if (!base::PathExists(log_file)) { 254 BrowserThread::PostDelayedTask( 255 BrowserThread::FILE, 256 FROM_HERE, 257 base::Bind(&Backend::GetBootTimesAndRunCallback, 258 this, is_canceled_cb, callback), 259 base::TimeDelta::FromMilliseconds(kReadAttemptDelayMs)); 260 return; 261 } 262 263 BootTimes boot_times; 264 265 GetTime(kFirmwareBootTime, &boot_times.firmware); 266 GetTime(uptime_prefix + kPreStartup, &boot_times.pre_startup); 267 GetTime(uptime_prefix + kXStarted, &boot_times.x_started); 268 GetTime(uptime_prefix + kChromeExec, &boot_times.chrome_exec); 269 GetTime(uptime_prefix + kChromeMain, &boot_times.chrome_main); 270 GetTime(uptime_prefix + kLoginPromptReady, &boot_times.login_prompt_ready); 271 272 boot_times.total = boot_times.firmware + boot_times.login_prompt_ready; 273 if (boot_times.chrome_exec > 0) { 274 boot_times.system = boot_times.chrome_exec - boot_times.pre_startup; 275 boot_times.chrome = boot_times.login_prompt_ready - boot_times.chrome_exec; 276 } else { 277 boot_times.system = boot_times.login_prompt_ready - boot_times.pre_startup; 278 } 279 280 SendBootTimesToUMA(boot_times); 281 282 callback.Run(boot_times); 283} 284 285// Appends the given buffer into the file. Returns the number of bytes 286// written, or -1 on error. 287// TODO(satorux): Move this to file_util. 288static int AppendFile(const base::FilePath& file_path, 289 const char* data, 290 int size) { 291 FILE* file = file_util::OpenFile(file_path, "a"); 292 if (!file) { 293 return -1; 294 } 295 const int num_bytes_written = fwrite(data, 1, size, file); 296 file_util::CloseFile(file); 297 return num_bytes_written; 298} 299 300static void RecordStatsDelayed(const base::FilePath::StringType& name, 301 const std::string& uptime, 302 const std::string& disk) { 303 const base::FilePath log_path(kLogPath); 304 const base::FilePath uptime_output = 305 log_path.Append(base::FilePath(kUptimePrefix + name)); 306 const base::FilePath disk_output = 307 log_path.Append(base::FilePath(kDiskPrefix + name)); 308 309 // Append numbers to the files. 310 AppendFile(uptime_output, uptime.data(), uptime.size()); 311 AppendFile(disk_output, disk.data(), disk.size()); 312} 313 314// static 315void BootTimesLoader::WriteTimes( 316 const std::string base_name, 317 const std::string uma_name, 318 const std::string uma_prefix, 319 std::vector<TimeMarker> login_times) { 320 const int kMinTimeMillis = 1; 321 const int kMaxTimeMillis = 30000; 322 const int kNumBuckets = 100; 323 const base::FilePath log_path(kLoginLogPath); 324 325 // Need to sort by time since the entries may have been pushed onto the 326 // vector (on the UI thread) in a different order from which they were 327 // created (potentially on other threads). 328 std::sort(login_times.begin(), login_times.end()); 329 330 base::Time first = login_times.front().time(); 331 base::Time last = login_times.back().time(); 332 base::TimeDelta total = last - first; 333 base::HistogramBase* total_hist = base::Histogram::FactoryTimeGet( 334 uma_name, 335 base::TimeDelta::FromMilliseconds(kMinTimeMillis), 336 base::TimeDelta::FromMilliseconds(kMaxTimeMillis), 337 kNumBuckets, 338 base::HistogramBase::kUmaTargetedHistogramFlag); 339 total_hist->AddTime(total); 340 std::string output = 341 base::StringPrintf("%s: %.2f", uma_name.c_str(), total.InSecondsF()); 342 base::Time prev = first; 343 for (unsigned int i = 0; i < login_times.size(); ++i) { 344 TimeMarker tm = login_times[i]; 345 base::TimeDelta since_first = tm.time() - first; 346 base::TimeDelta since_prev = tm.time() - prev; 347 std::string name; 348 349 if (tm.send_to_uma()) { 350 name = uma_prefix + tm.name(); 351 base::HistogramBase* prev_hist = base::Histogram::FactoryTimeGet( 352 name, 353 base::TimeDelta::FromMilliseconds(kMinTimeMillis), 354 base::TimeDelta::FromMilliseconds(kMaxTimeMillis), 355 kNumBuckets, 356 base::HistogramBase::kUmaTargetedHistogramFlag); 357 prev_hist->AddTime(since_prev); 358 } else { 359 name = tm.name(); 360 } 361 output += 362 base::StringPrintf( 363 "\n%.2f +%.4f %s", 364 since_first.InSecondsF(), 365 since_prev.InSecondsF(), 366 name.data()); 367 prev = tm.time(); 368 } 369 output += '\n'; 370 371 file_util::WriteFile( 372 log_path.Append(base_name), output.data(), output.size()); 373} 374 375void BootTimesLoader::LoginDone() { 376 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 377 AddLoginTimeMarker("LoginDone", false); 378 RecordCurrentStats(kChromeFirstRender); 379 registrar_.Remove(this, content::NOTIFICATION_LOAD_START, 380 content::NotificationService::AllSources()); 381 registrar_.Remove(this, content::NOTIFICATION_LOAD_STOP, 382 content::NotificationService::AllSources()); 383 registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 384 content::NotificationService::AllSources()); 385 registrar_.Remove( 386 this, 387 content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, 388 content::NotificationService::AllSources()); 389 // Don't swamp the FILE thread right away. 390 BrowserThread::PostDelayedTask( 391 BrowserThread::FILE, FROM_HERE, 392 base::Bind(&WriteTimes, kLoginTimes, kUmaLogin, kUmaLoginPrefix, 393 login_time_markers_), 394 base::TimeDelta::FromMilliseconds(kLoginTimeWriteDelayMs)); 395} 396 397void BootTimesLoader::WriteLogoutTimes() { 398 // Either we're on the browser thread, or (more likely) Chrome is in the 399 // process of shutting down and we're on the main thread but the message loop 400 // has already been terminated. 401 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) || 402 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)); 403 404 WriteTimes(kLogoutTimes, 405 kUmaLogout, 406 kUmaLogoutPrefix, 407 logout_time_markers_); 408} 409 410void BootTimesLoader::RecordStats(const std::string& name, const Stats& stats) { 411 BrowserThread::PostTask( 412 BrowserThread::FILE, FROM_HERE, 413 base::Bind(&RecordStatsDelayed, name, stats.uptime, stats.disk)); 414} 415 416BootTimesLoader::Stats BootTimesLoader::GetCurrentStats() { 417 const base::FilePath kProcUptime(FPL("/proc/uptime")); 418 const base::FilePath kDiskStat(FPL("/sys/block/sda/stat")); 419 Stats stats; 420 base::ThreadRestrictions::ScopedAllowIO allow_io; 421 file_util::ReadFileToString(kProcUptime, &stats.uptime); 422 file_util::ReadFileToString(kDiskStat, &stats.disk); 423 return stats; 424} 425 426void BootTimesLoader::RecordCurrentStats(const std::string& name) { 427 RecordStats(name, GetCurrentStats()); 428} 429 430void BootTimesLoader::SaveChromeMainStats() { 431 chrome_main_stats_ = GetCurrentStats(); 432} 433 434void BootTimesLoader::RecordChromeMainStats() { 435 RecordStats(kChromeMain, chrome_main_stats_); 436} 437 438void BootTimesLoader::RecordLoginAttempted() { 439 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 440 login_time_markers_.clear(); 441 AddLoginTimeMarker("LoginStarted", false); 442 if (!have_registered_) { 443 have_registered_ = true; 444 registrar_.Add(this, chrome::NOTIFICATION_LOGIN_AUTHENTICATION, 445 content::NotificationService::AllSources()); 446 registrar_.Add(this, content::NOTIFICATION_LOAD_START, 447 content::NotificationService::AllSources()); 448 registrar_.Add(this, content::NOTIFICATION_LOAD_STOP, 449 content::NotificationService::AllSources()); 450 registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 451 content::NotificationService::AllSources()); 452 registrar_.Add( 453 this, 454 content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, 455 content::NotificationService::AllSources()); 456 } 457} 458 459void BootTimesLoader::AddLoginTimeMarker( 460 const std::string& marker_name, bool send_to_uma) { 461 AddMarker(&login_time_markers_, TimeMarker(marker_name, send_to_uma)); 462} 463 464void BootTimesLoader::AddLogoutTimeMarker( 465 const std::string& marker_name, bool send_to_uma) { 466 AddMarker(&logout_time_markers_, TimeMarker(marker_name, send_to_uma)); 467} 468 469// static 470void BootTimesLoader::AddMarker(std::vector<TimeMarker>* vector, 471 TimeMarker marker) 472{ 473 // The marker vectors can only be safely manipulated on the main thread. 474 // If we're late in the process of shutting down (eg. as can be the case at 475 // logout), then we have to assume we're on the main thread already. 476 if (BrowserThread::CurrentlyOn(BrowserThread::UI) || 477 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)) { 478 vector->push_back(marker); 479 } else { 480 // Add the marker on the UI thread. 481 // Note that it's safe to use an unretained pointer to the vector because 482 // BootTimesLoader's lifetime exceeds that of the UI thread message loop. 483 BrowserThread::PostTask( 484 BrowserThread::UI, FROM_HERE, 485 base::Bind(&BootTimesLoader::AddMarker, 486 base::Unretained(vector), 487 marker)); 488 } 489} 490 491void BootTimesLoader::Observe( 492 int type, 493 const content::NotificationSource& source, 494 const content::NotificationDetails& details) { 495 switch (type) { 496 case chrome::NOTIFICATION_LOGIN_AUTHENTICATION: { 497 content::Details<AuthenticationNotificationDetails> auth_details(details); 498 if (auth_details->success()) { 499 AddLoginTimeMarker("Authenticate", true); 500 RecordCurrentStats(kLoginSuccess); 501 registrar_.Remove(this, chrome::NOTIFICATION_LOGIN_AUTHENTICATION, 502 content::NotificationService::AllSources()); 503 } 504 break; 505 } 506 case content::NOTIFICATION_LOAD_START: { 507 NavigationController* tab = 508 content::Source<NavigationController>(source).ptr(); 509 RenderWidgetHost* rwh = GetRenderWidgetHost(tab); 510 DCHECK(rwh); 511 AddLoginTimeMarker("TabLoad-Start: " + GetTabUrl(rwh), false); 512 render_widget_hosts_loading_.insert(rwh); 513 break; 514 } 515 case content::NOTIFICATION_LOAD_STOP: { 516 NavigationController* tab = 517 content::Source<NavigationController>(source).ptr(); 518 RenderWidgetHost* rwh = GetRenderWidgetHost(tab); 519 if (render_widget_hosts_loading_.find(rwh) != 520 render_widget_hosts_loading_.end()) { 521 AddLoginTimeMarker("TabLoad-End: " + GetTabUrl(rwh), false); 522 } 523 break; 524 } 525 case content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE: { 526 RenderWidgetHost* rwh = content::Source<RenderWidgetHost>(source).ptr(); 527 if (render_widget_hosts_loading_.find(rwh) != 528 render_widget_hosts_loading_.end()) { 529 AddLoginTimeMarker("TabPaint: " + GetTabUrl(rwh), false); 530 LoginDone(); 531 } 532 break; 533 } 534 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: { 535 WebContents* web_contents = content::Source<WebContents>(source).ptr(); 536 RenderWidgetHost* render_widget_host = 537 GetRenderWidgetHost(&web_contents->GetController()); 538 render_widget_hosts_loading_.erase(render_widget_host); 539 break; 540 } 541 default: 542 break; 543 } 544} 545 546} // namespace chromeos 547