Add Stability.ChildFrameCrash.Visibility for tracking sad *sub*frames.

This CL adds Stability.ChildFrameCrash.Visibility which is logged after
detecting that a sad subframe is shown.  The logged enum describes the
timing of a renderer crash in relation to whether the subframe is
visible or not.

Bug: 841493
Change-Id: Ic893764817bd1a9aa31684d53f858a398a31dfc8
Reviewed-on: https://chromium-review.googlesource.com/1054099
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Steven Holte <[email protected]>
Reviewed-by: Lucas Gadani <[email protected]>
Commit-Queue: Alex Moshchuk <[email protected]>
Cr-Commit-Position: refs/heads/master@{#559356}
diff --git a/content/browser/frame_host/cross_process_frame_connector.cc b/content/browser/frame_host/cross_process_frame_connector.cc
index b60a547..c374cb9 100644
--- a/content/browser/frame_host/cross_process_frame_connector.cc
+++ b/content/browser/frame_host/cross_process_frame_connector.cc
@@ -5,12 +5,14 @@
 #include "content/browser/frame_host/cross_process_frame_connector.h"
 
 #include "base/bind.h"
+#include "base/metrics/histogram_macros.h"
 #include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
 #include "components/viz/service/surfaces/surface.h"
 #include "components/viz/service/surfaces/surface_hittest.h"
 #include "content/browser/compositor/surface_utils.h"
 #include "content/browser/frame_host/frame_tree.h"
 #include "content/browser/frame_host/frame_tree_node.h"
+#include "content/browser/frame_host/render_frame_host_delegate.h"
 #include "content/browser/frame_host/render_frame_host_manager.h"
 #include "content/browser/frame_host/render_frame_proxy_host.h"
 #include "content/browser/renderer_host/cursor_manager.h"
@@ -44,6 +46,13 @@
 }
 
 CrossProcessFrameConnector::~CrossProcessFrameConnector() {
+  if (!IsVisible()) {
+    // MaybeLogCrash will check 1) if there was a crash or not and 2) if the
+    // crash might have been already logged earlier as kCrashedWhileVisible or
+    // kShownAfterCrashing.
+    MaybeLogCrash(CrashVisibility::kNeverVisibleAfterCrash);
+  }
+
   // Notify the view of this object being destroyed, if the view still exists.
   SetView(nullptr);
 }
@@ -95,6 +104,14 @@
   // visibility in case the frame owner is hidden in parent process. We should
   // try to move these updates to a single IPC (see https://crbug.com/750179).
   if (view_) {
+    if (has_crashed_ && !IsVisible()) {
+      // MaybeLogCrash will check 1) if there was a crash or not and 2) if the
+      // crash might have been already logged earlier as kCrashedWhileVisible or
+      // kShownAfterCrashing.
+      MaybeLogCrash(CrashVisibility::kNeverVisibleAfterCrash);
+    }
+    is_crash_already_logged_ = has_crashed_ = false;
+
     view_->SetFrameConnectorDelegate(this);
     if (is_hidden_)
       OnVisibilityChanged(false);
@@ -107,6 +124,21 @@
 }
 
 void CrossProcessFrameConnector::RenderProcessGone() {
+  has_crashed_ = true;
+
+  FrameTreeNode* node = frame_proxy_in_parent_renderer_->frame_tree_node();
+  int process_id = node->current_frame_host()->GetProcess()->GetID();
+  for (node = node->parent(); node; node = node->parent()) {
+    if (node->current_frame_host()->GetProcess()->GetID() == process_id) {
+      // The crash will be already logged by the ancestor - ignore this crash in
+      // the current instance of the CrossProcessFrameConnector.
+      is_crash_already_logged_ = true;
+    }
+  }
+
+  if (IsVisible())
+    MaybeLogCrash(CrashVisibility::kCrashedWhileVisible);
+
   frame_proxy_in_parent_renderer_->Send(new FrameMsg_ChildFrameProcessGone(
       frame_proxy_in_parent_renderer_->GetRoutingID()));
 }
@@ -300,10 +332,23 @@
   if (view_)
     view_->UpdateViewportIntersection(viewport_intersection,
                                       compositor_visible_rect);
+
+  if (IsVisible()) {
+    // MaybeLogCrash will check 1) if there was a crash or not and 2) if the
+    // crash might have been already logged earlier as kCrashedWhileVisible or
+    // kShownAfterCrashing.
+    MaybeLogCrash(CrashVisibility::kShownAfterCrashing);
+  }
 }
 
 void CrossProcessFrameConnector::OnVisibilityChanged(bool visible) {
   is_hidden_ = !visible;
+  if (IsVisible()) {
+    // MaybeLogCrash will check 1) if there was a crash or not and 2) if the
+    // crash might have been already logged earlier as kCrashedWhileVisible or
+    // kShownAfterCrashing.
+    MaybeLogCrash(CrashVisibility::kShownAfterCrashing);
+  }
   if (!view_)
     return;
 
@@ -474,4 +519,34 @@
   return subtree_throttled_;
 }
 
+void CrossProcessFrameConnector::MaybeLogCrash(CrashVisibility visibility) {
+  if (!has_crashed_)
+    return;
+
+  // Only log once per renderer crash.
+  if (is_crash_already_logged_)
+    return;
+  is_crash_already_logged_ = true;
+
+  // Actually log the UMA.
+  UMA_HISTOGRAM_ENUMERATION("Stability.ChildFrameCrash.Visibility", visibility);
+}
+
+bool CrossProcessFrameConnector::IsVisible() {
+  if (is_hidden_)
+    return false;
+  if (viewport_intersection_rect().IsEmpty())
+    return false;
+
+  Visibility embedder_visibility =
+      frame_proxy_in_parent_renderer_->frame_tree_node()
+          ->current_frame_host()
+          ->delegate()
+          ->GetVisibility();
+  if (embedder_visibility != Visibility::VISIBLE)
+    return false;
+
+  return true;
+}
+
 }  // namespace content
diff --git a/content/browser/frame_host/cross_process_frame_connector.h b/content/browser/frame_host/cross_process_frame_connector.h
index e07095dc..778d6ab 100644
--- a/content/browser/frame_host/cross_process_frame_connector.h
+++ b/content/browser/frame_host/cross_process_frame_connector.h
@@ -127,6 +127,26 @@
     return GetRootRenderWidgetHostView();
   }
 
+  // This enum backs a histogram - please do not modify or remove the existing
+  // enum values below (adding new values is okay, but please remember to also
+  // update enums.xml in this case). See enums.xml for descriptions of enum
+  // values.
+  enum class CrashVisibility {
+    kCrashedWhileVisible = 0,
+    kShownAfterCrashing = 1,
+    kNeverVisibleAfterCrash = 2,
+    kMaxValue = kNeverVisibleAfterCrash
+  };
+  // Logs the Stability.ChildFrameCrash.Visibility metric after checking that a
+  // crash has indeed happened and checking that the crash has not already been
+  // logged in UMA.
+  void MaybeLogCrash(CrashVisibility visibility);
+
+  // Returns whether the child widget is actually visible to the user.  This is
+  // different from the IsHidden override, and takes into account viewport
+  // intersection as well as the visibility of the RenderFrameHostDelegate.
+  bool IsVisible();
+
  private:
   friend class MockCrossProcessFrameConnector;
 
@@ -160,6 +180,14 @@
 
   bool is_scroll_bubbling_;
 
+  // Used to make sure we only log UMA once per renderer crash.
+  bool is_crash_already_logged_ = false;
+
+  // Used to make sure that MaybeLogCrash only logs the UMA in case of an actual
+  // crash (in case it is called from the destructor of
+  // CrossProcessFrameConnector or when WebContentsImpl::WasShown is called).
+  bool has_crashed_ = false;
+
   // The last pre-transform frame size received from the parent renderer.
   // |last_received_local_frame_size_| may be in DIP if use zoom for DSF is
   // off.
diff --git a/content/browser/frame_host/interstitial_page_impl.cc b/content/browser/frame_host/interstitial_page_impl.cc
index f7340d1f..6df976b 100644
--- a/content/browser/frame_host/interstitial_page_impl.cc
+++ b/content/browser/frame_host/interstitial_page_impl.cc
@@ -781,6 +781,11 @@
   }
 }
 
+Visibility InterstitialPageImpl::GetVisibility() const {
+  // Interstitials always occlude the underlying web content.
+  return Visibility::OCCLUDED;
+}
+
 void InterstitialPageImpl::CreateNewWidget(int32_t render_process_id,
                                            int32_t route_id,
                                            mojom::WidgetPtr widget,
diff --git a/content/browser/frame_host/interstitial_page_impl.h b/content/browser/frame_host/interstitial_page_impl.h
index 2edb67e8..d23262f 100644
--- a/content/browser/frame_host/interstitial_page_impl.h
+++ b/content/browser/frame_host/interstitial_page_impl.h
@@ -137,6 +137,7 @@
                          const gfx::Rect& initial_rect,
                          bool user_gesture) override;
   void SetFocusedFrame(FrameTreeNode* node, SiteInstance* source) override;
+  Visibility GetVisibility() const override;
 
   // RenderViewHostDelegate implementation:
   RenderViewHostDelegateView* GetDelegateView() override;
diff --git a/content/browser/frame_host/render_frame_host_delegate.cc b/content/browser/frame_host/render_frame_host_delegate.cc
index 017610a..390bcf0 100644
--- a/content/browser/frame_host/render_frame_host_delegate.cc
+++ b/content/browser/frame_host/render_frame_host_delegate.cc
@@ -122,4 +122,8 @@
   return false;
 }
 
+Visibility RenderFrameHostDelegate::GetVisibility() const {
+  return Visibility::HIDDEN;
+}
+
 }  // namespace content
diff --git a/content/browser/frame_host/render_frame_host_delegate.h b/content/browser/frame_host/render_frame_host_delegate.h
index 465b165..cd4a295 100644
--- a/content/browser/frame_host/render_frame_host_delegate.h
+++ b/content/browser/frame_host/render_frame_host_delegate.h
@@ -17,6 +17,7 @@
 #include "content/common/content_export.h"
 #include "content/common/frame_message_enums.h"
 #include "content/public/browser/site_instance.h"
+#include "content/public/browser/visibility.h"
 #include "content/public/common/javascript_dialog_type.h"
 #include "content/public/common/media_stream_request.h"
 #include "content/public/common/resource_load_info.mojom.h"
@@ -371,6 +372,9 @@
   virtual void UpdatePictureInPictureSurfaceId(const viz::SurfaceId& surface_id,
                                                const gfx::Size& natural_size) {}
 
+  // Returns the visibility of the delegate.
+  virtual Visibility GetVisibility() const;
+
  protected:
   virtual ~RenderFrameHostDelegate() {}
 };
diff --git a/content/browser/site_per_process_browsertest.cc b/content/browser/site_per_process_browsertest.cc
index 426695a4..be66d1c3 100644
--- a/content/browser/site_per_process_browsertest.cc
+++ b/content/browser/site_per_process_browsertest.cc
@@ -33,6 +33,7 @@
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind_test_util.h"
+#include "base/test/histogram_tester.h"
 #include "base/test/test_timeouts.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -11831,4 +11832,186 @@
   EXPECT_TRUE(success);
 }
 
+IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
+                       ChildFrameCrashMetrics_KilledWhileVisible) {
+  // Set-up a frame tree that helps verify what the metrics tracks:
+  // 1) frames (12 frames are affected if B process gets killed) or
+  // 2) crashes (simply 1 crash if B process gets killed)?
+  // 3) widgets (10 b widgets and 1 c widget are affected if B is killed,
+  //    but a sad frame will appear only in 9 widgets - this excludes
+  //    widgets for the b,c(b) part of the frame tree) or
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b(b,c(b)),b,b,b,b,b,b,b,b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetFrameTree()->root();
+
+  // Kill the child frame.
+  base::HistogramTester histograms;
+  RenderProcessHost* child_process =
+      root->child_at(0)->current_frame_host()->GetProcess();
+  RenderProcessHostWatcher crash_observer(
+      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  child_process->Shutdown(0);
+  crash_observer.Wait();
+
+  // Verify that the expected metrics got logged.
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kCrashedWhileVisible, 9);
+
+  // Hide and show the web contents and verify that no more metrics got logged.
+  web_contents()->UpdateWebContentsVisibility(Visibility::HIDDEN);
+  web_contents()->UpdateWebContentsVisibility(Visibility::VISIBLE);
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kCrashedWhileVisible, 9);
+}
+
+IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
+                       ChildFrameCrashMetrics_KilledMainFrame) {
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(a(b(b,c)))"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetFrameTree()->root();
+
+  // Kill the main frame.
+  base::HistogramTester histograms;
+  RenderProcessHost* child_process = root->current_frame_host()->GetProcess();
+  RenderProcessHostWatcher crash_observer(
+      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  child_process->Shutdown(0);
+  crash_observer.Wait();
+
+  // Verify that no child frame metrics got logged.
+  histograms.ExpectTotalCount("Stability.ChildFrameCrash.Visibility", 0);
+}
+
+IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
+                       ChildFrameCrashMetrics_KilledWhileHiddenThenShown) {
+  // Set-up a frame tree that helps verify what the metrics tracks:
+  // 1) frames (12 frames are affected if B process gets killed) or
+  // 2) widgets (10 b widgets and 1 c widget are affected if B is killed) or
+  // 3) crashes (1 crash if B process gets killed)?
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b(b,c),b,b,b,b,b,b,b,b,b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetFrameTree()->root();
+
+  // Hide the web contents (UpdateWebContentsVisibility is called twice to avoid
+  // hitting the |!did_first_set_visible_| case).
+  web_contents()->UpdateWebContentsVisibility(Visibility::VISIBLE);
+  web_contents()->UpdateWebContentsVisibility(Visibility::HIDDEN);
+
+  // Kill the subframe.
+  base::HistogramTester histograms;
+  RenderProcessHost* child_process =
+      root->child_at(0)->current_frame_host()->GetProcess();
+  RenderProcessHostWatcher crash_observer(
+      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  child_process->Shutdown(0);
+  crash_observer.Wait();
+
+  // Verify that no child frame metrics got logged (yet - while WebContents are
+  // hidden).
+  histograms.ExpectTotalCount("Stability.ChildFrameCrash.Visibility", 0);
+
+  // Show the web contents.
+  // and verify that the expected metrics got logged.
+  web_contents()->UpdateWebContentsVisibility(Visibility::VISIBLE);
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kShownAfterCrashing, 10);
+
+  // Hide and show the web contents again and verify that no more metrics got
+  // logged.
+  web_contents()->UpdateWebContentsVisibility(Visibility::HIDDEN);
+  web_contents()->UpdateWebContentsVisibility(Visibility::VISIBLE);
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kShownAfterCrashing, 10);
+}
+
+IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
+                       ChildFrameCrashMetrics_NeverShown) {
+  // Set-up a frame tree that helps verify what the metrics tracks:
+  // 1) frames (12 frames are affected if B process gets killed) or
+  // 2) widgets (10 b widgets and 1 c widget are affected if B is killed) or
+  // 3) crashes (1 crash if B process gets killed)?
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b(b,c),b,b,b,b,b,b,b,b,b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetFrameTree()->root();
+
+  // Hide the web contents (UpdateWebContentsVisibility is called twice to avoid
+  // hitting the |!did_first_set_visible_| case).
+  web_contents()->UpdateWebContentsVisibility(Visibility::VISIBLE);
+  web_contents()->UpdateWebContentsVisibility(Visibility::HIDDEN);
+
+  // Kill the subframe.
+  base::HistogramTester histograms;
+  RenderProcessHost* child_process =
+      root->child_at(0)->current_frame_host()->GetProcess();
+  RenderProcessHostWatcher crash_observer(
+      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  child_process->Shutdown(0);
+  crash_observer.Wait();
+
+  // Navigate away - this will trigger logging of the UMA.
+  EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kNeverVisibleAfterCrash, 10);
+}
+
+IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
+                       ChildFrameCrashMetrics_ScrolledIntoView) {
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetFrameTree()->root();
+
+  // Fill the main frame so that the subframe is pushed below the fold (is
+  // scrolled outside of the current view) and wait until the main frame redraws
+  // itself (i.e. making sure CPFC::OnUpdateViewportIntersection has arrived).
+  std::string filling_script = R"(
+    var frame = document.body.querySelectorAll("iframe")[0];
+    for (var i = 0; i < 100; i++) {
+      var p = document.createElement("p");
+      p.innerText = "blah";
+      document.body.insertBefore(p, frame);
+    }
+  )";
+  EXPECT_TRUE(ExecuteScript(root, filling_script));
+  MainThreadFrameObserver main_widget_observer(
+      root->current_frame_host()->GetRenderWidgetHost());
+  main_widget_observer.Wait();
+
+  // Kill the child frame.
+  base::HistogramTester histograms;
+  RenderProcessHost* child_process =
+      root->child_at(0)->current_frame_host()->GetProcess();
+  RenderProcessHostWatcher crash_observer(
+      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  child_process->Shutdown(0);
+  crash_observer.Wait();
+
+  // Verify that no child frame metrics got logged (yet - while the subframe is
+  // below the fold / is not scrolled into view).
+  histograms.ExpectTotalCount("Stability.ChildFrameCrash.Visibility", 0);
+
+  // Scroll the subframe into view and wait until the scrolled frame draws
+  // itself.
+  std::string scrolling_script = R"(
+    var frame = document.body.querySelectorAll("iframe")[0];
+    frame.scrollIntoView();
+  )";
+  EXPECT_TRUE(ExecuteScript(root, scrolling_script));
+  main_widget_observer.Wait();
+
+  // Verify that the expected metrics got logged.
+  histograms.ExpectUniqueSample(
+      "Stability.ChildFrameCrash.Visibility",
+      CrossProcessFrameConnector::CrashVisibility::kShownAfterCrashing, 1);
+}
+
 }  // namespace content
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index d0d68b4..76feed1 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -1520,6 +1520,19 @@
 
   last_active_time_ = base::TimeTicks::Now();
   SetVisibility(Visibility::VISIBLE);
+
+  for (FrameTreeNode* node : frame_tree_.Nodes()) {
+    RenderFrameProxyHost* parent = node->render_manager()->GetProxyToParent();
+    if (!parent)
+      continue;
+
+    if (parent->cross_process_frame_connector()->IsVisible()) {
+      // MaybeLogCrash will check 1) if there was a crash or not and 2) if the
+      // crash might have been already logged earlier as kCrashedWhileVisible.
+      parent->cross_process_frame_connector()->MaybeLogCrash(
+          CrossProcessFrameConnector::CrashVisibility::kShownAfterCrashing);
+    }
+  }
 }
 
 void WebContentsImpl::WasHidden() {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index dc33d7e..c74a30e 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -7986,6 +7986,28 @@
   <int value="250477278" label="(Delphi exception)"/>
 </enum>
 
+<enum name="CrashVisibility">
+  <summary>
+    This enum describes the timing of a renderer crash in relation to whether a
+    sad frame was visible.  Whether a subframe is &quot;visible&quot; is
+    currently defined as 1) in visible, non-occluded WebContents and 2) with a
+    non-empty viewport intersection rect.
+  </summary>
+  <int value="0" label="CrashedWhileVisible">
+    The renderer crashed while the subframe was visible.
+  </int>
+  <int value="1" label="ShownAfterCrashing">
+    The renderer crashed while the subframe was hidden, but later the frame
+    became visible (the tab was foregrounded, the frame was scrolled into view,
+    the frame was resized, etc.).
+  </int>
+  <int value="2" label="NeverVisibleAfterCrash">
+    The renderer crashed while the subframe was hidden, and the frame was never
+    shown afterwards (e.g. until eventually being discarded when the whole tab
+    is navigated away).
+  </int>
+</enum>
+
 <enum name="CreatePersistentHistogramResult">
   <int value="0" label="Success: Histogram created in persistent space."/>
   <int value="1" label="Error: Invalid metadata pointer. (coding error)"/>
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index 843407e..d63a434 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -90564,6 +90564,16 @@
   </summary>
 </histogram>
 
+<histogram name="Stability.ChildFrameCrash.Visibility" enum="CrashVisibility">
+  <owner>[email protected]</owner>
+  <owner>[email protected]</owner>
+  <owner>[email protected]</owner>
+  <summary>
+    Logged after detecting that a sad subframe is shown (logged at most once per
+    crash).
+  </summary>
+</histogram>
+
 <histogram name="Stability.CrashedProcessAge.Extension" units="ms">
   <owner>[email protected]</owner>
   <summary>