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/files/file_path.h" 12 #include "base/files/file_util.h" 13 #include "base/json/json_reader.h" 14 #include "base/json/json_writer.h" 15 #include "base/lazy_instance.h" 16 #include "base/location.h" 17 #include "base/message_loop/message_loop.h" 18 #include "base/message_loop/message_loop_proxy.h" 19 #include "base/metrics/histogram.h" 20 #include "base/prefs/pref_service.h" 21 #include "base/strings/string_number_conversions.h" 22 #include "base/strings/string_util.h" 23 #include "base/strings/stringprintf.h" 24 #include "base/threading/thread.h" 25 #include "base/threading/thread_restrictions.h" 26 #include "base/time/time.h" 27 #include "chrome/browser/browser_process.h" 28 #include "chrome/browser/chrome_notification_types.h" 29 #include "chrome/browser/ui/browser.h" 30 #include "chrome/browser/ui/browser_iterator.h" 31 #include "chrome/browser/ui/tabs/tab_strip_model.h" 32 #include "chrome/common/chrome_switches.h" 33 #include "chrome/common/pref_names.h" 34 #include "components/user_manager/user_manager.h" 35 #include "content/public/browser/browser_thread.h" 36 #include "content/public/browser/navigation_controller.h" 37 #include "content/public/browser/notification_service.h" 38 #include "content/public/browser/render_widget_host_view.h" 39 #include "content/public/browser/web_contents.h" 40 41 using content::BrowserThread; 42 using content::NavigationController; 43 using content::RenderWidgetHost; 44 using content::RenderWidgetHostView; 45 using content::WebContents; 46 47 namespace { 48 49 const char kUptime[] = "uptime"; 50 const char kDisk[] = "disk"; 51 52 RenderWidgetHost* GetRenderWidgetHost(NavigationController* tab) { 53 WebContents* web_contents = tab->GetWebContents(); 54 if (web_contents) { 55 RenderWidgetHostView* render_widget_host_view = 56 web_contents->GetRenderWidgetHostView(); 57 if (render_widget_host_view) 58 return render_widget_host_view->GetRenderWidgetHost(); 59 } 60 return NULL; 61 } 62 63 const std::string GetTabUrl(RenderWidgetHost* rwh) { 64 RenderWidgetHostView* rwhv = rwh->GetView(); 65 for (chrome::BrowserIterator it; !it.done(); it.Next()) { 66 Browser* browser = *it; 67 for (int i = 0, tab_count = browser->tab_strip_model()->count(); 68 i < tab_count; 69 ++i) { 70 WebContents* tab = browser->tab_strip_model()->GetWebContentsAt(i); 71 if (tab->GetRenderWidgetHostView() == rwhv) { 72 return tab->GetLastCommittedURL().spec(); 73 } 74 } 75 } 76 return std::string(); 77 } 78 79 // Appends the given buffer into the file. Returns the number of bytes 80 // written, or -1 on error. 81 // TODO(satorux): Move this to file_util. 82 int AppendFile(const base::FilePath& file_path, const char* data, int size) { 83 FILE* file = base::OpenFile(file_path, "a"); 84 if (!file) 85 return -1; 86 87 const int num_bytes_written = fwrite(data, 1, size, file); 88 base::CloseFile(file); 89 return num_bytes_written; 90 } 91 92 } // namespace 93 94 namespace chromeos { 95 96 #define FPL(value) FILE_PATH_LITERAL(value) 97 98 // Dir uptime & disk logs are located in. 99 static const base::FilePath::CharType kLogPath[] = FPL("/tmp"); 100 // Dir log{in,out} logs are located in. 101 static const base::FilePath::CharType kLoginLogPath[] = 102 FPL("/home/chronos/user"); 103 // Prefix for the time measurement files. 104 static const base::FilePath::CharType kUptimePrefix[] = FPL("uptime-"); 105 // Prefix for the disk usage files. 106 static const base::FilePath::CharType kDiskPrefix[] = FPL("disk-"); 107 // Name of the time that Chrome's main() is called. 108 static const base::FilePath::CharType kChromeMain[] = FPL("chrome-main"); 109 // Delay in milliseconds before writing the login times to disk. 110 static const int64 kLoginTimeWriteDelayMs = 3000; 111 112 // Names of login stats files. 113 static const base::FilePath::CharType kLoginSuccess[] = FPL("login-success"); 114 static const base::FilePath::CharType kChromeFirstRender[] = 115 FPL("chrome-first-render"); 116 117 // Names of login UMA values. 118 static const char kUmaLogin[] = "BootTime.Login"; 119 static const char kUmaLoginNewUser[] = "BootTime.LoginNewUser"; 120 static const char kUmaLoginPrefix[] = "BootTime."; 121 static const char kUmaLogout[] = "ShutdownTime.Logout"; 122 static const char kUmaLogoutPrefix[] = "ShutdownTime."; 123 static const char kUmaRestart[] = "ShutdownTime.Restart"; 124 125 // Name of file collecting login times. 126 static const base::FilePath::CharType kLoginTimes[] = FPL("login-times"); 127 128 // Name of file collecting logout times. 129 static const char kLogoutTimes[] = "logout-times"; 130 131 static base::LazyInstance<BootTimesLoader> g_boot_times_loader = 132 LAZY_INSTANCE_INITIALIZER; 133 134 // static 135 BootTimesLoader::Stats BootTimesLoader::Stats::GetCurrentStats() { 136 const base::FilePath kProcUptime(FPL("/proc/uptime")); 137 const base::FilePath kDiskStat(FPL("/sys/block/sda/stat")); 138 Stats stats; 139 // Callers of this method expect synchronous behavior. 140 // It's safe to allow IO here, because only virtual FS are accessed. 141 base::ThreadRestrictions::ScopedAllowIO allow_io; 142 base::ReadFileToString(kProcUptime, &stats.uptime_); 143 base::ReadFileToString(kDiskStat, &stats.disk_); 144 return stats; 145 } 146 147 std::string BootTimesLoader::Stats::SerializeToString() const { 148 if (uptime_.empty() && disk_.empty()) 149 return std::string(); 150 base::DictionaryValue dictionary; 151 dictionary.SetString(kUptime, uptime_); 152 dictionary.SetString(kDisk, disk_); 153 154 std::string result; 155 if (!base::JSONWriter::Write(&dictionary, &result)) { 156 LOG(WARNING) << "BootTimesLoader::Stats::SerializeToString(): failed."; 157 return std::string(); 158 } 159 160 return result; 161 } 162 163 // static 164 BootTimesLoader::Stats BootTimesLoader::Stats::DeserializeFromString( 165 const std::string& source) { 166 if (source.empty()) 167 return Stats(); 168 169 scoped_ptr<base::Value> value(base::JSONReader::Read(source)); 170 base::DictionaryValue* dictionary; 171 if (!value || !value->GetAsDictionary(&dictionary)) { 172 LOG(ERROR) << "BootTimesLoader::Stats::DeserializeFromString(): not a " 173 "dictionary: '" << source << "'"; 174 return Stats(); 175 } 176 177 Stats result; 178 if (!dictionary->GetString(kUptime, &result.uptime_) || 179 !dictionary->GetString(kDisk, &result.disk_)) { 180 LOG(ERROR) 181 << "BootTimesLoader::Stats::DeserializeFromString(): format error: '" 182 << source << "'"; 183 return Stats(); 184 } 185 186 return result; 187 } 188 189 bool BootTimesLoader::Stats::UptimeDouble(double* result) const { 190 std::string uptime = uptime_; 191 const size_t space_at = uptime.find_first_of(' '); 192 if (space_at == std::string::npos) 193 return false; 194 195 uptime.resize(space_at); 196 197 if (base::StringToDouble(uptime, result)) 198 return true; 199 200 return false; 201 } 202 203 void BootTimesLoader::Stats::RecordStats(const std::string& name) const { 204 BrowserThread::PostBlockingPoolTask( 205 FROM_HERE, 206 base::Bind(&BootTimesLoader::Stats::RecordStatsImpl, 207 base::Owned(new Stats(*this)), 208 name)); 209 } 210 211 void BootTimesLoader::Stats::RecordStatsWithCallback( 212 const std::string& name, 213 const base::Closure& callback) const { 214 BrowserThread::PostBlockingPoolTaskAndReply( 215 FROM_HERE, 216 base::Bind(&BootTimesLoader::Stats::RecordStatsImpl, 217 base::Owned(new Stats(*this)), 218 name), 219 callback); 220 } 221 222 void BootTimesLoader::Stats::RecordStatsImpl( 223 const base::FilePath::StringType& name) const { 224 DCHECK(content::BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread()); 225 226 const base::FilePath log_path(kLogPath); 227 const base::FilePath uptime_output = 228 log_path.Append(base::FilePath(kUptimePrefix + name)); 229 const base::FilePath disk_output = 230 log_path.Append(base::FilePath(kDiskPrefix + name)); 231 232 // Append numbers to the files. 233 AppendFile(uptime_output, uptime_.data(), uptime_.size()); 234 AppendFile(disk_output, disk_.data(), disk_.size()); 235 } 236 237 BootTimesLoader::BootTimesLoader() 238 : backend_(new Backend()), 239 have_registered_(false), 240 login_done_(false), 241 restart_requested_(false) { 242 login_time_markers_.reserve(30); 243 logout_time_markers_.reserve(30); 244 } 245 246 BootTimesLoader::~BootTimesLoader() { 247 } 248 249 // static 250 BootTimesLoader* BootTimesLoader::Get() { 251 return g_boot_times_loader.Pointer(); 252 } 253 254 // static 255 void BootTimesLoader::WriteTimes( 256 const std::string base_name, 257 const std::string uma_name, 258 const std::string uma_prefix, 259 std::vector<TimeMarker> login_times) { 260 const int kMinTimeMillis = 1; 261 const int kMaxTimeMillis = 30000; 262 const int kNumBuckets = 100; 263 const base::FilePath log_path(kLoginLogPath); 264 265 // Need to sort by time since the entries may have been pushed onto the 266 // vector (on the UI thread) in a different order from which they were 267 // created (potentially on other threads). 268 std::sort(login_times.begin(), login_times.end()); 269 270 base::Time first = login_times.front().time(); 271 base::Time last = login_times.back().time(); 272 base::TimeDelta total = last - first; 273 base::HistogramBase* total_hist = base::Histogram::FactoryTimeGet( 274 uma_name, 275 base::TimeDelta::FromMilliseconds(kMinTimeMillis), 276 base::TimeDelta::FromMilliseconds(kMaxTimeMillis), 277 kNumBuckets, 278 base::HistogramBase::kUmaTargetedHistogramFlag); 279 total_hist->AddTime(total); 280 std::string output = 281 base::StringPrintf("%s: %.2f", uma_name.c_str(), total.InSecondsF()); 282 base::Time prev = first; 283 for (unsigned int i = 0; i < login_times.size(); ++i) { 284 TimeMarker tm = login_times[i]; 285 base::TimeDelta since_first = tm.time() - first; 286 base::TimeDelta since_prev = tm.time() - prev; 287 std::string name; 288 289 if (tm.send_to_uma()) { 290 name = uma_prefix + tm.name(); 291 base::HistogramBase* prev_hist = base::Histogram::FactoryTimeGet( 292 name, 293 base::TimeDelta::FromMilliseconds(kMinTimeMillis), 294 base::TimeDelta::FromMilliseconds(kMaxTimeMillis), 295 kNumBuckets, 296 base::HistogramBase::kUmaTargetedHistogramFlag); 297 prev_hist->AddTime(since_prev); 298 } else { 299 name = tm.name(); 300 } 301 output += 302 base::StringPrintf( 303 "\n%.2f +%.4f %s", 304 since_first.InSecondsF(), 305 since_prev.InSecondsF(), 306 name.data()); 307 prev = tm.time(); 308 } 309 output += '\n'; 310 311 base::WriteFile(log_path.Append(base_name), output.data(), output.size()); 312 } 313 314 void BootTimesLoader::LoginDone(bool is_user_new) { 315 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 316 if (login_done_) 317 return; 318 319 login_done_ = true; 320 AddLoginTimeMarker("LoginDone", false); 321 RecordCurrentStats(kChromeFirstRender); 322 if (have_registered_) { 323 registrar_.Remove(this, 324 content::NOTIFICATION_LOAD_START, 325 content::NotificationService::AllSources()); 326 registrar_.Remove(this, 327 content::NOTIFICATION_LOAD_STOP, 328 content::NotificationService::AllSources()); 329 registrar_.Remove(this, 330 content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 331 content::NotificationService::AllSources()); 332 registrar_.Remove( 333 this, 334 content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, 335 content::NotificationService::AllSources()); 336 } 337 // Don't swamp the FILE thread right away. 338 BrowserThread::PostDelayedTask( 339 BrowserThread::FILE, 340 FROM_HERE, 341 base::Bind(&WriteTimes, 342 kLoginTimes, 343 (is_user_new ? kUmaLoginNewUser : kUmaLogin), 344 kUmaLoginPrefix, 345 login_time_markers_), 346 base::TimeDelta::FromMilliseconds(kLoginTimeWriteDelayMs)); 347 } 348 349 void BootTimesLoader::WriteLogoutTimes() { 350 // Either we're on the browser thread, or (more likely) Chrome is in the 351 // process of shutting down and we're on the main thread but the message loop 352 // has already been terminated. 353 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) || 354 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)); 355 356 WriteTimes(kLogoutTimes, 357 (restart_requested_ ? kUmaRestart : kUmaLogout), 358 kUmaLogoutPrefix, 359 logout_time_markers_); 360 } 361 362 // static 363 void BootTimesLoader::ClearLogoutStartedLastPreference() { 364 PrefService* local_state = g_browser_process->local_state(); 365 local_state->ClearPref(prefs::kLogoutStartedLast); 366 } 367 368 void BootTimesLoader::OnChromeProcessStart() { 369 PrefService* local_state = g_browser_process->local_state(); 370 const std::string logout_started_last_str = 371 local_state->GetString(prefs::kLogoutStartedLast); 372 if (logout_started_last_str.empty()) 373 return; 374 375 // Note that kLogoutStartedLast is not cleared on format error to stay in 376 // logs in case of other fatal system errors. 377 378 const Stats logout_started_last_stats = 379 Stats::DeserializeFromString(logout_started_last_str); 380 if (logout_started_last_stats.uptime().empty()) 381 return; 382 383 double logout_started_last; 384 double uptime; 385 if (!logout_started_last_stats.UptimeDouble(&logout_started_last) || 386 !Stats::GetCurrentStats().UptimeDouble(&uptime)) { 387 return; 388 } 389 390 if (logout_started_last >= uptime) { 391 // Reboot happened. 392 ClearLogoutStartedLastPreference(); 393 return; 394 } 395 396 // Write /tmp/uptime-logout-started as well. 397 const char kLogoutStarted[] = "logout-started"; 398 logout_started_last_stats.RecordStatsWithCallback( 399 kLogoutStarted, 400 base::Bind(&BootTimesLoader::ClearLogoutStartedLastPreference)); 401 } 402 403 void BootTimesLoader::OnLogoutStarted(PrefService* state) { 404 const std::string uptime = Stats::GetCurrentStats().SerializeToString(); 405 if (!uptime.empty()) 406 state->SetString(prefs::kLogoutStartedLast, uptime); 407 } 408 409 void BootTimesLoader::RecordCurrentStats(const std::string& name) { 410 Stats::GetCurrentStats().RecordStats(name); 411 } 412 413 void BootTimesLoader::SaveChromeMainStats() { 414 chrome_main_stats_ = Stats::GetCurrentStats(); 415 } 416 417 void BootTimesLoader::RecordChromeMainStats() { 418 chrome_main_stats_.RecordStats(kChromeMain); 419 } 420 421 void BootTimesLoader::RecordLoginAttempted() { 422 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 423 if (login_done_) 424 return; 425 426 login_time_markers_.clear(); 427 AddLoginTimeMarker("LoginStarted", false); 428 if (!have_registered_) { 429 have_registered_ = true; 430 registrar_.Add(this, content::NOTIFICATION_LOAD_START, 431 content::NotificationService::AllSources()); 432 registrar_.Add(this, content::NOTIFICATION_LOAD_STOP, 433 content::NotificationService::AllSources()); 434 registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 435 content::NotificationService::AllSources()); 436 registrar_.Add( 437 this, 438 content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE, 439 content::NotificationService::AllSources()); 440 } 441 } 442 443 void BootTimesLoader::AddLoginTimeMarker( 444 const std::string& marker_name, bool send_to_uma) { 445 AddMarker(&login_time_markers_, TimeMarker(marker_name, send_to_uma)); 446 } 447 448 void BootTimesLoader::AddLogoutTimeMarker( 449 const std::string& marker_name, bool send_to_uma) { 450 AddMarker(&logout_time_markers_, TimeMarker(marker_name, send_to_uma)); 451 } 452 453 // static 454 void BootTimesLoader::AddMarker(std::vector<TimeMarker>* vector, 455 TimeMarker marker) 456 { 457 // The marker vectors can only be safely manipulated on the main thread. 458 // If we're late in the process of shutting down (eg. as can be the case at 459 // logout), then we have to assume we're on the main thread already. 460 if (BrowserThread::CurrentlyOn(BrowserThread::UI) || 461 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)) { 462 vector->push_back(marker); 463 } else { 464 // Add the marker on the UI thread. 465 // Note that it's safe to use an unretained pointer to the vector because 466 // BootTimesLoader's lifetime exceeds that of the UI thread message loop. 467 BrowserThread::PostTask( 468 BrowserThread::UI, FROM_HERE, 469 base::Bind(&BootTimesLoader::AddMarker, 470 base::Unretained(vector), 471 marker)); 472 } 473 } 474 475 void BootTimesLoader::RecordAuthenticationSuccess() { 476 AddLoginTimeMarker("Authenticate", true); 477 RecordCurrentStats(kLoginSuccess); 478 } 479 480 void BootTimesLoader::RecordAuthenticationFailure() { 481 // Do nothing for now. 482 } 483 484 void BootTimesLoader::Observe( 485 int type, 486 const content::NotificationSource& source, 487 const content::NotificationDetails& details) { 488 switch (type) { 489 case content::NOTIFICATION_LOAD_START: { 490 NavigationController* tab = 491 content::Source<NavigationController>(source).ptr(); 492 RenderWidgetHost* rwh = GetRenderWidgetHost(tab); 493 DCHECK(rwh); 494 AddLoginTimeMarker("TabLoad-Start: " + GetTabUrl(rwh), false); 495 render_widget_hosts_loading_.insert(rwh); 496 break; 497 } 498 case content::NOTIFICATION_LOAD_STOP: { 499 NavigationController* tab = 500 content::Source<NavigationController>(source).ptr(); 501 RenderWidgetHost* rwh = GetRenderWidgetHost(tab); 502 if (render_widget_hosts_loading_.find(rwh) != 503 render_widget_hosts_loading_.end()) { 504 AddLoginTimeMarker("TabLoad-End: " + GetTabUrl(rwh), false); 505 } 506 break; 507 } 508 case content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE: { 509 RenderWidgetHost* rwh = content::Source<RenderWidgetHost>(source).ptr(); 510 if (render_widget_hosts_loading_.find(rwh) != 511 render_widget_hosts_loading_.end()) { 512 AddLoginTimeMarker("TabPaint: " + GetTabUrl(rwh), false); 513 LoginDone(user_manager::UserManager::Get()->IsCurrentUserNew()); 514 } 515 break; 516 } 517 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: { 518 WebContents* web_contents = content::Source<WebContents>(source).ptr(); 519 RenderWidgetHost* render_widget_host = 520 GetRenderWidgetHost(&web_contents->GetController()); 521 render_widget_hosts_loading_.erase(render_widget_host); 522 break; 523 } 524 default: 525 break; 526 } 527 } 528 529 } // namespace chromeos 530