Add metrics to crostini installer.

Currently, there are a significant number of install failures we don't
have much visibility into, especially on low-powered devices. This CL
adds metrics for free disk space and time to install, and records
failures due to network flake in the correct bucket, to give us a
better understanding of this.

Change-Id: I34ce13eabb698a9962db3da8cfe76eb48e33c3b4
Bug: 953545
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1569369
Reviewed-by: Mark Pearson <mpearson@chromium.org>
Reviewed-by: Nicholas Verne <nverne@chromium.org>
Commit-Queue: Fergus Dall <sidereal@google.com>
Auto-Submit: Fergus Dall <sidereal@google.com>
Cr-Commit-Position: refs/heads/master@{#652041}
diff --git a/chrome/browser/ui/views/crostini/crostini_installer_view.cc b/chrome/browser/ui/views/crostini/crostini_installer_view.cc
index d0770a9..09076bc 100644
--- a/chrome/browser/ui/views/crostini/crostini_installer_view.cc
+++ b/chrome/browser/ui/views/crostini/crostini_installer_view.cc
@@ -10,6 +10,7 @@
 #include "ash/public/cpp/ash_typography.h"
 #include "base/bind.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
 #include "base/numerics/ranges.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
@@ -56,6 +57,8 @@
 // TODO(timloh): This is just a placeholder.
 constexpr int kDownloadSizeInBytes = 300 * 1024 * 1024;
 
+constexpr int kUninitializedDiskSpace = -1;
+
 constexpr gfx::Insets kOOBEButtonRowInsets(32, 64, 32, 64);
 constexpr int kOOBEWindowWidth = 768;
 // TODO(timloh): The button row's preferred height (48px) adds to this. I'm not
@@ -64,12 +67,22 @@
 constexpr int kOOBEWindowHeight = 640 - 48;
 constexpr int kLinuxIllustrationWidth = 448;
 constexpr int kLinuxIllustrationHeight = 180;
+constexpr base::FilePath::CharType kHomeDirectory[] =
+    FILE_PATH_LITERAL("/home");
 
 constexpr char kCrostiniSetupResultHistogram[] = "Crostini.SetupResult";
 constexpr char kCrostiniSetupSourceHistogram[] = "Crostini.SetupSource";
 constexpr char kCrostiniTimeFromDeviceSetupToInstall[] =
     "Crostini.TimeFromDeviceSetupToInstall";
 constexpr char kCrostiniDiskImageSizeHistogram[] = "Crostini.DiskImageSize";
+constexpr char kCrostiniTimeToInstallSuccess[] =
+    "Crostini.TimeToInstallSuccess";
+constexpr char kCrostiniTimeToInstallCancel[] = "Crostini.TimeToInstallCancel";
+constexpr char kCrostiniTimeToInstallError[] = "Crostini.TimeToInstallError";
+constexpr char kCrostiniAvailableDiskSuccess[] =
+    "Crostini.AvailableDiskSuccess";
+constexpr char kCrostiniAvailableDiskCancel[] = "Crostini.AvailableDiskCancel";
+constexpr char kCrostiniAvailableDiskError[] = "Crostini.AvailableDiskError";
 
 void RecordTimeFromDeviceSetupToInstallMetric() {
   base::PostTaskWithTraitsAndReplyWithResult(
@@ -118,6 +131,18 @@
 
   crostini::CrostiniManager::GetForProfile(profile)->SetInstallerViewStatus(
       true);
+
+  base::PostTaskWithTraitsAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
+                     base::FilePath(kHomeDirectory)),
+      base::BindOnce(
+          &CrostiniInstallerView::OnAvailableDiskSpace,
+          g_crostini_installer_view->weak_ptr_factory_.GetWeakPtr()));
+}
+
+void CrostiniInstallerView::OnAvailableDiskSpace(int64_t bytes) {
+  free_disk_space_ = bytes;
 }
 
 int CrostiniInstallerView::GetDialogButtons() const {
@@ -165,6 +190,7 @@
 
   UpdateState(State::INSTALL_START);
   profile_->GetPrefs()->SetBoolean(crostini::prefs::kCrostiniEnabled, true);
+  install_start_time_ = base::TimeTicks::Now();
 
   // The default value of kCrostiniContainers is set to migrate existing
   // crostini users who don't have the pref set. If crostini is being installed,
@@ -200,6 +226,19 @@
 }
 
 bool CrostiniInstallerView::Cancel() {
+  if (!has_logged_timing_result_ &&
+      restart_id_ == crostini::CrostiniManager::kUninitializedRestartId) {
+    UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallCancel,
+                             base::TimeTicks::Now() - install_start_time_);
+    has_logged_timing_result_ = true;
+  }
+  if (!has_logged_free_disk_result_ &&
+      restart_id_ == crostini::CrostiniManager::kUninitializedRestartId &&
+      free_disk_space_ != kUninitializedDiskSpace) {
+    base::UmaHistogramCounts1M(kCrostiniAvailableDiskCancel,
+                               free_disk_space_ >> 20);
+    has_logged_free_disk_result_ = true;
+  }
   if (state_ != State::INSTALL_END && state_ != State::CLEANUP &&
       state_ != State::CLEANUP_FINISHED &&
       restart_id_ != crostini::CrostiniManager::kUninitializedRestartId) {
@@ -254,10 +293,18 @@
   DCHECK_EQ(state_, State::INSTALL_IMAGE_LOADER);
 
   if (result != CrostiniResult::SUCCESS) {
-    LOG(ERROR) << "Failed to install the cros-termina component";
-    HandleError(
-        l10n_util::GetStringUTF16(IDS_CROSTINI_INSTALLER_LOAD_TERMINA_ERROR),
-        SetupResult::kErrorLoadingTermina);
+    if (content::GetNetworkConnectionTracker()->IsOffline()) {
+      LOG(ERROR) << "Network connection dropped while downloading cros-termina";
+      const base::string16 device_type = ui::GetChromeOSDeviceName();
+      HandleError(l10n_util::GetStringFUTF16(
+                      IDS_CROSTINI_INSTALLER_OFFLINE_ERROR, device_type),
+                  SetupResult::kErrorOffline);
+    } else {
+      LOG(ERROR) << "Failed to install the cros-termina component";
+      HandleError(
+          l10n_util::GetStringUTF16(IDS_CROSTINI_INSTALLER_LOAD_TERMINA_ERROR),
+          SetupResult::kErrorLoadingTermina);
+    }
     return;
   }
   VLOG(1) << "cros-termina install success";
@@ -334,11 +381,19 @@
   DCHECK_EQ(state_, State::SETUP_CONTAINER);
 
   if (result != CrostiniResult::SUCCESS) {
-    LOG(ERROR) << "Failed to set up container with error code: "
-               << static_cast<int>(result);
-    HandleError(
-        l10n_util::GetStringUTF16(IDS_CROSTINI_INSTALLER_SETUP_CONTAINER_ERROR),
-        SetupResult::kErrorSettingUpContainer);
+    if (content::GetNetworkConnectionTracker()->IsOffline()) {
+      LOG(ERROR) << "Network connection dropped while downloading container";
+      const base::string16 device_type = ui::GetChromeOSDeviceName();
+      HandleError(l10n_util::GetStringFUTF16(
+                      IDS_CROSTINI_INSTALLER_OFFLINE_ERROR, device_type),
+                  SetupResult::kErrorOffline);
+    } else {
+      LOG(ERROR) << "Failed to set up container with error code: "
+                 << static_cast<int>(result);
+      HandleError(l10n_util::GetStringUTF16(
+                      IDS_CROSTINI_INSTALLER_SETUP_CONTAINER_ERROR),
+                  SetupResult::kErrorSettingUpContainer);
+    }
     return;
   }
   VLOG(1) << "Set up container successfully";
@@ -393,7 +448,9 @@
 }
 
 CrostiniInstallerView::CrostiniInstallerView(Profile* profile)
-    : profile_(profile), weak_ptr_factory_(this) {
+    : profile_(profile),
+      free_disk_space_(kUninitializedDiskSpace),
+      weak_ptr_factory_(this) {
   // Layout constants from the spec.
   constexpr gfx::Insets kDialogInsets(60, 64, 0, 64);
   constexpr int kDialogSpacingVertical = 32;
@@ -504,6 +561,18 @@
   if (state_ == State::ERROR)
     return;
 
+  if (!has_logged_timing_result_) {
+    UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallError,
+                             base::TimeTicks::Now() - install_start_time_);
+    has_logged_timing_result_ = true;
+  }
+  if (!has_logged_free_disk_result_ &&
+      free_disk_space_ != kUninitializedDiskSpace) {
+    base::UmaHistogramCounts1M(kCrostiniAvailableDiskError,
+                               free_disk_space_ >> 20);
+    has_logged_free_disk_result_ = true;
+  }
+
   RecordSetupResultHistogram(result);
   restart_id_ = crostini::CrostiniManager::kUninitializedRestartId;
   UpdateState(State::ERROR);
@@ -545,6 +614,17 @@
   RecordSetupResultHistogram(SetupResult::kSuccess);
   crostini_manager->UpdateLaunchMetricsForEnterpriseReporting();
   RecordTimeFromDeviceSetupToInstallMetric();
+  if (!has_logged_timing_result_) {
+    UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallSuccess,
+                             base::TimeTicks::Now() - install_start_time_);
+    has_logged_timing_result_ = true;
+  }
+  if (!has_logged_free_disk_result_ &&
+      free_disk_space_ != kUninitializedDiskSpace) {
+    base::UmaHistogramCounts1M(kCrostiniAvailableDiskSuccess,
+                               free_disk_space_ >> 20);
+    has_logged_free_disk_result_ = true;
+  }
   GetWidget()->Close();
 }
 
diff --git a/chrome/browser/ui/views/crostini/crostini_installer_view.h b/chrome/browser/ui/views/crostini/crostini_installer_view.h
index 5b84d025..f208a5e4 100644
--- a/chrome/browser/ui/views/crostini/crostini_installer_view.h
+++ b/chrome/browser/ui/views/crostini/crostini_installer_view.h
@@ -135,6 +135,8 @@
 
   void RecordSetupResultHistogram(SetupResult result);
 
+  void OnAvailableDiskSpace(int64_t bytes);
+
   State state_ = State::PROMPT;
   views::ImageView* logo_image_ = nullptr;
   views::Label* big_message_label_ = nullptr;
@@ -149,6 +151,8 @@
   base::Time state_start_time_;
   std::unique_ptr<base::RepeatingTimer> state_progress_timer_;
   bool do_cleanup_ = true;
+  base::TimeTicks install_start_time_;
+  int64_t free_disk_space_;
 
   // Whether the result has been logged or not is stored to prevent multiple
   // results being logged for a given setup flow. This can happen due to
@@ -156,6 +160,10 @@
   // able to hit Cancel after any errors occur.
   bool has_logged_result_ = false;
 
+  bool has_logged_timing_result_ = false;
+
+  bool has_logged_free_disk_result_ = false;
+
   base::RepeatingCallback<void(double)> progress_bar_callback_for_testing_;
   base::OnceClosure quit_closure_for_testing_;
 
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index eb0eff1..5395e1d 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -20715,6 +20715,50 @@
   </summary>
 </histogram>
 
+<histogram name="Crostini.AvailableDiskCancel" units="MiB"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The available disk space at the start of the crostini install flow, recorded
+    when installation was canceled. This is only recorded if the user cancels
+    the install before it finishes. It is not recorded if the user did not start
+    the install (i.e. pressed cancel before pressing accept), or if the
+    cancellation happens after an error, including if they press retry. For
+    clarity, at most one of Crostini.AvailableDiskCancel,
+    Crostini.AvailableDiskError, and Crostini.AvailableDiskSuccess will be
+    recorded between opening the installer view and closing it.
+  </summary>
+</histogram>
+
+<histogram name="Crostini.AvailableDiskError" units="MiB"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The available disk space at the start of the crostini install flow, recorded
+    when installation returned an error. This is only recorded the first time an
+    error occurs, it is not re-recorded if the user presses retry and an error
+    occurs again. For clarity, at most one of Crostini.AvailableDiskCancel,
+    Crostini.AvailableDiskError, and Crostini.AvailableDiskSuccess will be
+    recorded between opening the installer view and closing it.
+  </summary>
+</histogram>
+
+<histogram name="Crostini.AvailableDiskSuccess" units="MiB"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The available disk space at the start of the crostini install flow, recorded
+    when installation succeeded. This is not recorded if an error occurred, the
+    user pressed retry, and the install succeeded on a second or subsequent
+    attempt. For clarity, at most one of Crostini.AvailableDiskCancel,
+    Crostini.AvailableDiskError, and Crostini.AvailableDiskSuccess will be
+    recorded between opening the installer view and closing it.
+  </summary>
+</histogram>
+
 <histogram name="Crostini.Backup" enum="CrostiniExportContainerResult"
     expires_after="2020-01-01">
   <owner>joelhockey@chromium.org</owner>
@@ -20824,6 +20868,50 @@
   </summary>
 </histogram>
 
+<histogram name="Crostini.TimeToInstallCancel" units="ms"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The time taken for the crostini installer to be canceled by the user. This
+    is only recorded if the user cancels the install before it finishes. It is
+    not recorded if the user did not start the install (i.e. pressed cancel
+    before pressing accept), or if the cancellation happens after an error,
+    including if they press retry. For clarity, at most one of
+    Crostini.TimeToInstallCancel, Crostini.TimeToInstallError, and
+    Crostini.TimeToInstallSuccess will be recorded between opening the installer
+    view and closing it.
+  </summary>
+</histogram>
+
+<histogram name="Crostini.TimeToInstallError" units="ms"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The time taken for the crostini installer to fail due to an error. This is
+    only recorded the first time an error occurs, it is not re-recorded if the
+    user presses retry and an error occurs again. For clarity, at most one of
+    Crostini.TimeToInstallCancel, Crostini.TimeToInstallError, and
+    Crostini.TimeToInstallSuccess will be recorded between opening the installer
+    view and closing it.
+  </summary>
+</histogram>
+
+<histogram name="Crostini.TimeToInstallSuccess" units="ms"
+    expires_after="2020-04-16">
+  <owner>sidereal@google.com</owner>
+  <owner>nverne@chromium.org</owner>
+  <summary>
+    The time taken for the crostini installer to finish successfully. This is
+    not recorded if an error occurred, the user pressed retry, and the install
+    succeeded on a second or subsequent attempt. For clarity, at most one of
+    Crostini.TimeToInstallCancel, Crostini.TimeToInstallError, and
+    Crostini.TimeToInstallSuccess will be recorded between opening the installer
+    view and closing it.
+  </summary>
+</histogram>
+
 <histogram name="Crostini.UninstallResult" enum="CrostiniUninstallResult">
   <owner>benwells@chromium.org</owner>
   <owner>tbuckley@chromium.org</owner>