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