Fix thumbnail capture timing.

Thumbnails are now captured at a time that makes sense for tab previews
rather than the old NTP timing (which was largely when switching away
from or leaving a page).

Bug: 928954
Change-Id: I79e2ae1bd066de753a6f6b592639f0db09f35fd3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1531640
Commit-Queue: Dana Fried <dfried@chromium.org>
Reviewed-by: Collin Baker <collinbaker@chromium.org>
Cr-Commit-Position: refs/heads/master@{#642920}
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index f29cbc5..ed58c47 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1062,6 +1062,8 @@
       "thumbnails/thumbnail_tab_helper.h",
       "thumbnails/thumbnail_utils.cc",
       "thumbnails/thumbnail_utils.h",
+      "thumbnails/thumbnail_web_contents_observer.cc",
+      "thumbnails/thumbnail_web_contents_observer.h",
       "toolbar/app_menu_icon_controller.cc",
       "toolbar/app_menu_icon_controller.h",
       "toolbar/app_menu_model.cc",
diff --git a/chrome/browser/ui/thumbnails/thumbnail_image.cc b/chrome/browser/ui/thumbnails/thumbnail_image.cc
index d139e210..b71fd63d 100644
--- a/chrome/browser/ui/thumbnails/thumbnail_image.cc
+++ b/chrome/browser/ui/thumbnails/thumbnail_image.cc
@@ -66,11 +66,10 @@
 
   base::PostTaskWithTraitsAndReplyWithResult(
       FROM_HERE,
-      {base::TaskPriority::BEST_EFFORT,
+      {base::TaskPriority::USER_VISIBLE,
        base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
       base::BindOnce(&ThumbnailData::ToImageSkia, image_representation_),
       std::move(callback));
-
   return true;
 }
 
@@ -98,7 +97,7 @@
                                        CreateThumbnailCallback callback) {
   base::PostTaskWithTraitsAndReplyWithResult(
       FROM_HERE,
-      {base::TaskPriority::BEST_EFFORT,
+      {base::TaskPriority::USER_VISIBLE,
        base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
       base::BindOnce(&ThumbnailData::FromSkBitmap, bitmap),
       base::BindOnce(
diff --git a/chrome/browser/ui/thumbnails/thumbnail_tab_helper.cc b/chrome/browser/ui/thumbnails/thumbnail_tab_helper.cc
index ac77bea..06ff85b1 100644
--- a/chrome/browser/ui/thumbnails/thumbnail_tab_helper.cc
+++ b/chrome/browser/ui/thumbnails/thumbnail_tab_helper.cc
@@ -5,277 +5,223 @@
 #include "chrome/browser/ui/thumbnails/thumbnail_tab_helper.h"
 
 #include "base/bind.h"
-#include "base/feature_list.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/task/post_task.h"
 #include "base/task/task_traits.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/tabs/tab_style.h"
 #include "chrome/browser/ui/thumbnails/thumbnail_utils.h"
-#include "chrome/common/chrome_features.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
-#include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/render_view_host.h"
 #include "content/public/browser/render_widget_host.h"
 #include "content/public/browser/render_widget_host_view.h"
+#include "content/public/browser/web_contents.h"
 #include "third_party/skia/include/core/SkBitmap.h"
-#include "ui/gfx/color_utils.h"
 #include "ui/gfx/scrollbar_size.h"
 
 ThumbnailTabHelper::ThumbnailTabHelper(content::WebContents* contents)
-    : content::WebContentsObserver(contents) {}
+    : ThumbnailWebContentsObserver(contents),
+      view_is_visible_(contents->GetVisibility() ==
+                       content::Visibility::VISIBLE) {}
 
 ThumbnailTabHelper::~ThumbnailTabHelper() = default;
 
-void ThumbnailTabHelper::RenderWidgetHostVisibilityChanged(
-    content::RenderWidgetHost* widget_host,
-    bool became_visible) {
-  if (!became_visible)
-    TabHidden();
+void ThumbnailTabHelper::TopLevelNavigationStarted(const GURL& url) {
+  UpdateCurrentUrl(url);
 }
 
-void ThumbnailTabHelper::RenderWidgetHostDestroyed(
-    content::RenderWidgetHost* widget_host) {
-  observer_.Remove(widget_host);
+void ThumbnailTabHelper::TopLevelNavigationEnded(const GURL& url) {
+  UpdateCurrentUrl(url);
 }
 
-void ThumbnailTabHelper::RenderViewCreated(
-    content::RenderViewHost* render_view_host) {
-  StartWatchingRenderViewHost(render_view_host);
-}
-
-void ThumbnailTabHelper::RenderViewHostChanged(
-    content::RenderViewHost* old_host,
-    content::RenderViewHost* new_host) {
-  StopWatchingRenderViewHost(old_host);
-  StartWatchingRenderViewHost(new_host);
-}
-
-void ThumbnailTabHelper::RenderViewDeleted(
-    content::RenderViewHost* render_view_host) {
-  StopWatchingRenderViewHost(render_view_host);
-}
-
-void ThumbnailTabHelper::DidStartNavigation(
-    content::NavigationHandle* navigation_handle) {
-  if (!navigation_handle->IsInMainFrame() ||
-      navigation_handle->IsSameDocument()) {
-    return;
-  }
-
-  // At this point, the new navigation has just been started, but the
-  // WebContents still shows the previous page. Grab a thumbnail before it
-  // goes away.
-  StartThumbnailCaptureIfNecessary(TriggerReason::NAVIGATING_AWAY);
-
-  // Now reset navigation-related state. It's important that this happens after
-  // calling StartThumbnailCaptureIfNecessary.
-  did_navigation_finish_ = false;
-  has_received_document_since_navigation_finished_ = false;
-  has_painted_since_document_received_ = false;
-  // Reset the page transition to some uninteresting type, since the actual
-  // type isn't available at this point. We'll get it in DidFinishNavigation
-  // (if that happens, which isn't guaranteed).
-  page_transition_ = ui::PAGE_TRANSITION_LINK;
-}
-
-void ThumbnailTabHelper::DidFinishNavigation(
-    content::NavigationHandle* navigation_handle) {
-  if (!navigation_handle->HasCommitted() ||
-      !navigation_handle->IsInMainFrame() ||
-      navigation_handle->IsSameDocument()) {
-    return;
-  }
-  did_navigation_finish_ = true;
-  page_transition_ = navigation_handle->GetPageTransition();
-}
-
-void ThumbnailTabHelper::DocumentAvailableInMainFrame() {
-  // If there's currently a screen capture going on, ignore its result.
-  // Otherwise there's a risk that we'll get a picture of the wrong page.
-  // Note: It *looks* like WebContentsObserver::DidFirstVisuallyNonEmptyPaint
-  // would be a better signal for this, but it uses a weird heuristic to detect
-  // "visually non empty" paints, so it might not be entirely safe.
-  waiting_for_capture_ = false;
-
-  // Mark that we got the document, unless we're in the middle of a navigation.
-  // In that case, this refers to the previous document, but we're tracking the
-  // state of the new one.
-  if (did_navigation_finish_) {
-    // From now on, we'll start watching for paint events.
-    has_received_document_since_navigation_finished_ = true;
+void ThumbnailTabHelper::UpdateCurrentUrl(const GURL& url) {
+  current_url_ = url;
+  if (current_url_ != thumbnail_url_ &&
+      thumbnail_state_ != ThumbnailState::kNoThumbnail) {
+    thumbnail_state_ = ThumbnailState::kNoThumbnail;
+    thumbnail_ = ThumbnailImage();
+    NotifyTabPreviewChanged();
   }
 }
 
-void ThumbnailTabHelper::DocumentOnLoadCompletedInMainFrame() {
-  // Usually, DocumentAvailableInMainFrame always gets called first, so this one
-  // shouldn't be necessary. However, DocumentAvailableInMainFrame is not fired
-  // for empty documents (i.e. about:blank), which are thus handled here.
-  DocumentAvailableInMainFrame();
+void ThumbnailTabHelper::PageLoadStarted(FrameContext frame_context) {
+  if (frame_context == FrameContext::kMainFrame)
+    is_loading_ = true;
+  ScheduleThumbnailCapture(CaptureSchedule::kDelayed);
 }
 
-void ThumbnailTabHelper::DidFirstVisuallyNonEmptyPaint() {
-  // If we haven't gotten the current document since navigating, then this paint
-  // refers to the *previous* document, so ignore it.
-  if (has_received_document_since_navigation_finished_) {
-    has_painted_since_document_received_ = true;
+void ThumbnailTabHelper::PageLoadFinished(FrameContext frame_context) {
+  if (frame_context == FrameContext::kMainFrame)
+    is_loading_ = false;
+  ScheduleThumbnailCapture(CaptureSchedule::kAttemptImmediate);
+}
+
+void ThumbnailTabHelper::PageUpdated(FrameContext frame_context) {
+  ScheduleThumbnailCapture(CaptureSchedule::kAttemptImmediate);
+}
+
+void ThumbnailTabHelper::VisibilityChanged(bool visible) {
+  // When the visibility of the current tab changes (most importantly, when the
+  // user is switching away from the current tab) we want to capture a snapshot
+  // of the tab to capture e.g. its scroll position, so that the preview will
+  // look like the tab did when the user last switched to/from it.
+  const bool was_visible = view_is_visible_;
+  view_is_visible_ = visible;
+  if (was_visible != visible) {
+    // Because it can take a moment for tabs to re-render, use a delay when
+    // returning to a tab, but capture immediately when switching away.
+    const CaptureSchedule schedule =
+        visible ? CaptureSchedule::kDelayed : CaptureSchedule::kImmediate;
+    ScheduleThumbnailCapture(schedule);
   }
 }
 
-void ThumbnailTabHelper::DidStartLoading() {
-  load_interrupted_ = false;
-}
-
-void ThumbnailTabHelper::NavigationStopped() {
-  // This function gets called when the page loading is interrupted by the
-  // stop button.
-  load_interrupted_ = true;
-}
-
-void ThumbnailTabHelper::StartWatchingRenderViewHost(
-    content::RenderViewHost* render_view_host) {
-  // We get notified whenever a new RenderView is created, which does not
-  // necessarily come with a new RenderViewHost, and there is no good way to get
-  // notifications of new RenderViewHosts only. So just be tolerant of
-  // re-registrations.
-  content::RenderWidgetHost* render_widget_host = render_view_host->GetWidget();
-  if (!observer_.IsObserving(render_widget_host))
-    observer_.Add(render_widget_host);
-}
-
-void ThumbnailTabHelper::StopWatchingRenderViewHost(
-    content::RenderViewHost* render_view_host) {
-  if (!render_view_host) {
-    return;
-  }
-
-  content::RenderWidgetHost* render_widget_host = render_view_host->GetWidget();
-  if (observer_.IsObserving(render_widget_host))
-    observer_.Remove(render_widget_host);
-}
-
-void ThumbnailTabHelper::StartThumbnailCaptureIfNecessary(
-    TriggerReason trigger) {
+void ThumbnailTabHelper::ScheduleThumbnailCapture(CaptureSchedule schedule) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
-  // Don't take a screenshot if we haven't painted anything since the last
-  // navigation. This can happen when navigating away again very quickly.
-  if (!has_painted_since_document_received_) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_NO_PAINT_YET);
+  if (schedule != CaptureSchedule::kImmediate && !view_is_visible_)
+    return;
+
+  constexpr base::TimeDelta kDelayTime = base::TimeDelta::FromMilliseconds(250);
+  constexpr base::TimeDelta kMinTimeBetweenCaptures =
+      base::TimeDelta::FromMilliseconds(500);
+
+  // We will do the capture either now or at some point in the future.
+  base::TimeDelta delay;
+  if (schedule == CaptureSchedule::kDelayed)
+    delay += kDelayTime;
+
+  // The time until the next scheduled capture, or the time since the most
+  // recent (durations in the past are negative).
+  const base::TimeDelta until_scheduled =
+      last_scheduled_capture_time_ - base::TimeTicks::Now();
+
+  // If we would schedule a non-immediate capture too close to an existing
+  // capture, push it out or discard it altogether.
+  if (schedule != CaptureSchedule::kImmediate &&
+      delay - until_scheduled < kMinTimeBetweenCaptures) {
+    if (until_scheduled > delay)
+      return;
+    delay = until_scheduled + kMinTimeBetweenCaptures;
+  }
+
+  last_scheduled_capture_time_ = base::TimeTicks::Now() + delay;
+
+  if (delay.is_zero()) {
+    StartThumbnailCapture(schedule);
     return;
   }
 
-  // Ignore thumbnail update requests if one is already in progress.
-  if (thumbnailing_in_progress_) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_IN_PROGRESS);
+  base::PostDelayedTaskWithTraits(
+      FROM_HERE, {content::BrowserThread::UI},
+      base::BindOnce(&ThumbnailTabHelper::StartThumbnailCapture,
+                     weak_factory_.GetWeakPtr(), schedule),
+      delay);
+}
+
+void ThumbnailTabHelper::StartThumbnailCapture(CaptureSchedule schedule) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  CaptureInfo capture_info{web_contents()->GetVisibleURL(),
+                           is_loading_ ? ThumbnailState::kLoadInProgress
+                                       : ThumbnailState::kFinishedLoading};
+  DCHECK(!capture_info.url.is_empty());
+
+  if (!view_is_visible_ && schedule != CaptureSchedule::kImmediate)
     return;
-  }
 
   // Destroying a WebContents may trigger it to be hidden, prompting a snapshot
   // which would be unwise to attempt <http://crbug.com/130097>. If the
   // WebContents is in the middle of destruction, do not risk it.
-  if (!web_contents() || web_contents()->IsBeingDestroyed()) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_NO_WEBCONTENTS);
+  if (!web_contents() || web_contents()->IsBeingDestroyed())
     return;
-  }
 
-  // Note: Do *not* use GetLastVisibleURL - it might already have been updated
-  // for a new pending navigation. The committed URL is the one corresponding
-  // to the currently visible content.
-  const GURL& url = web_contents()->GetLastCommittedURL();
-  if (!url.is_valid()) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_NO_URL);
-    return;
-  }
+  base::TimeTicks start_time = base::TimeTicks::Now();
 
-  // Check if the thumbnail needs to be updated. If not, log and return.
+  content::RenderWidgetHostView* const source_view =
+      web_contents()->GetRenderViewHost()->GetWidget()->GetView();
 
-  content::RenderWidgetHost* render_widget_host =
-      web_contents()->GetRenderViewHost()->GetWidget();
-  content::RenderWidgetHostView* view = render_widget_host->GetView();
-  if (!view || !view->IsSurfaceAvailableForCopy()) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_VIEW_NOT_AVAILABLE);
+  // If there's no view or the view isn't available right now, put off
+  // capturing.
+  if (!source_view || !source_view->IsSurfaceAvailableForCopy()) {
+    ScheduleThumbnailCapture(CaptureSchedule::kDelayed);
     return;
   }
 
   // Note: this is the size in pixels on-screen, not the size in DIPs.
-  gfx::Size source_size = view->GetViewBounds().size();
+  gfx::Size source_size = source_view->GetViewBounds().size();
   // Clip the pixels that will commonly hold a scrollbar, which looks bad in
   // thumbnails.
-  const float scale_factor = view->GetDeviceScaleFactor();
+  const float scale_factor = source_view->GetDeviceScaleFactor();
   const int scrollbar_size = gfx::scrollbar_size() * scale_factor;
   source_size.Enlarge(-scrollbar_size, -scrollbar_size);
 
-  if (source_size.IsEmpty()) {
-    LogThumbnailingOutcome(trigger, Outcome::NOT_ATTEMPTED_EMPTY_RECT);
+  if (source_size.IsEmpty())
     return;
-  }
-
-  thumbnailing_in_progress_ = true;
 
   const gfx::Size desired_size = TabStyle::GetPreviewImageSize();
   thumbnails::CanvasCopyInfo copy_info =
       thumbnails::GetCanvasCopyInfo(source_size, scale_factor, desired_size);
-  copy_from_surface_start_time_ = base::TimeTicks::Now();
-  waiting_for_capture_ = true;
-  view->CopyFromSurface(
+
+  source_view->CopyFromSurface(
       copy_info.copy_rect, copy_info.target_size,
-      base::BindOnce(&ThumbnailTabHelper::ProcessCapturedBitmap,
-                     weak_factory_.GetWeakPtr(), trigger));
+      base::BindOnce(&ThumbnailTabHelper::ProcessCapturedThumbnail,
+                     weak_factory_.GetWeakPtr(), capture_info, start_time));
 }
 
-void ThumbnailTabHelper::ProcessCapturedBitmap(TriggerReason trigger,
-                                               const SkBitmap& bitmap) {
-  // If |waiting_for_capture_| is false, that means something happened in the
-  // meantime which makes the captured image unsafe to use.
-  bool was_canceled = !waiting_for_capture_;
-  waiting_for_capture_ = false;
+void ThumbnailTabHelper::ProcessCapturedThumbnail(
+    const CaptureInfo& capture_info,
+    base::TimeTicks start_time,
+    const SkBitmap& bitmap) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
-  base::TimeDelta copy_from_surface_time =
-      base::TimeTicks::Now() - copy_from_surface_start_time_;
+  DCHECK(!capture_info.url.is_empty());
+  DCHECK(capture_info.target_state != ThumbnailState::kNoThumbnail);
+
+  const base::TimeTicks finish_time = base::TimeTicks::Now();
+  const base::TimeDelta copy_from_surface_time = finish_time - start_time;
   UMA_HISTOGRAM_TIMES("Thumbnails.CopyFromSurfaceTime", copy_from_surface_time);
 
-  if (!bitmap.drawsNothing() && !was_canceled) {
-    // On success, we must be on the UI thread.
-    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-    // From here on, nothing can fail, so log success.
-    LogThumbnailingOutcome(trigger, Outcome::SUCCESS);
-    thumbnail_ = ThumbnailImage::FromSkBitmap(bitmap);
-    web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
-  } else {
-    LogThumbnailingOutcome(
-        trigger, was_canceled ? Outcome::CANCELED : Outcome::READBACK_FAILED);
-  }
-  thumbnailing_in_progress_ = false;
-}
-
-void ThumbnailTabHelper::TabHidden() {
-  // Skip if a pending entry exists. TabHidden can be called while navigating
-  // pages and this is not a time when thumbnails should be generated.
-  if (!web_contents() || web_contents()->GetController().GetPendingEntry()) {
-    LogThumbnailingOutcome(TriggerReason::TAB_HIDDEN,
-                           Outcome::NOT_ATTEMPTED_PENDING_NAVIGATION);
+  if (bitmap.drawsNothing()) {
+    // TODO(dfried): Log capture failed.
+    MaybeScheduleAnotherCapture(capture_info, finish_time);
     return;
   }
-  StartThumbnailCaptureIfNecessary(TriggerReason::TAB_HIDDEN);
+
+  // TODO(dfried): Log capture succeeded.
+  ThumbnailImage::FromSkBitmapAsync(
+      bitmap,
+      base::BindOnce(&ThumbnailTabHelper::StoreThumbnail,
+                     weak_factory_.GetWeakPtr(), capture_info, finish_time));
 }
 
-// static
-void ThumbnailTabHelper::LogThumbnailingOutcome(TriggerReason trigger,
-                                                Outcome outcome) {
-  UMA_HISTOGRAM_ENUMERATION("Thumbnails.CaptureOutcome", outcome,
-                            Outcome::COUNT);
+void ThumbnailTabHelper::StoreThumbnail(const CaptureInfo& capture_info,
+                                        base::TimeTicks start_time,
+                                        ThumbnailImage thumbnail) {
+  DCHECK(thumbnail.HasData());
+  const base::TimeTicks finish_time = base::TimeTicks::Now();
+  const base::TimeDelta process_time = finish_time - start_time;
+  UMA_HISTOGRAM_TIMES("Thumbnails.ProcessBitmapTime", process_time);
+  thumbnail_state_ = capture_info.target_state;
+  thumbnail_url_ = capture_info.url;
+  thumbnail_ = thumbnail;
 
-  switch (trigger) {
-    case TriggerReason::TAB_HIDDEN:
-      UMA_HISTOGRAM_ENUMERATION("Thumbnails.CaptureOutcome.TabHidden", outcome,
-                                Outcome::COUNT);
-      break;
-    case TriggerReason::NAVIGATING_AWAY:
-      UMA_HISTOGRAM_ENUMERATION("Thumbnails.CaptureOutcome.NavigatingAway",
-                                outcome, Outcome::COUNT);
-      break;
+  NotifyTabPreviewChanged();
+  MaybeScheduleAnotherCapture(capture_info, finish_time);
+}
+
+void ThumbnailTabHelper::NotifyTabPreviewChanged() {
+  web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
+}
+
+void ThumbnailTabHelper::MaybeScheduleAnotherCapture(
+    const CaptureInfo& capture_info,
+    base::TimeTicks finish_time) {
+  // If the page is still loading, schedule another capture a short time later.
+  if (capture_info.target_state != ThumbnailState::kFinishedLoading &&
+      finish_time > last_scheduled_capture_time_) {
+    ScheduleThumbnailCapture(CaptureSchedule::kDelayed);
   }
 }
 
diff --git a/chrome/browser/ui/thumbnails/thumbnail_tab_helper.h b/chrome/browser/ui/thumbnails/thumbnail_tab_helper.h
index a4fe69ec..687d8abd 100644
--- a/chrome/browser/ui/thumbnails/thumbnail_tab_helper.h
+++ b/chrome/browser/ui/thumbnails/thumbnail_tab_helper.h
@@ -7,107 +7,76 @@
 
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
-#include "base/scoped_observer.h"
 #include "base/time/time.h"
 #include "chrome/browser/ui/thumbnails/thumbnail_image.h"
-#include "content/public/browser/render_widget_host_observer.h"
-#include "content/public/browser/web_contents_observer.h"
+#include "chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.h"
 #include "content/public/browser/web_contents_user_data.h"
-#include "ui/base/page_transition_types.h"
-
-namespace content {
-class NavigationHandle;
-class RenderViewHost;
-}  // namespace content
 
 class ThumbnailTabHelper
-    : public content::RenderWidgetHostObserver,
-      public content::WebContentsObserver,
+    : public ThumbnailWebContentsObserver,
       public content::WebContentsUserData<ThumbnailTabHelper> {
  public:
+  enum class ThumbnailState {
+    kNoThumbnail,     // no thumbnail is available
+    kLoadInProgress,  // thumbnail available but is of a page that is loading
+    kFinishedLoading  // thumbnail should represent the finished page
+  };
+
   ~ThumbnailTabHelper() override;
 
+  ThumbnailState thumbnail_state() const { return thumbnail_state_; }
   ThumbnailImage thumbnail() const { return thumbnail_; }
 
+ protected:
+  // ThumbnailWebContentsObserver:
+  void TopLevelNavigationStarted(const GURL& url) override;
+  void TopLevelNavigationEnded(const GURL& url) override;
+  void PageLoadStarted(FrameContext frame_context) override;
+  void PageLoadFinished(FrameContext frame_context) override;
+  void PageUpdated(FrameContext frame_context) override;
+  void VisibilityChanged(bool visible) override;
+
  private:
+  enum class CaptureSchedule { kImmediate, kAttemptImmediate, kDelayed };
+
+  struct CaptureInfo {
+    GURL url;
+    ThumbnailState target_state;
+  };
+
   explicit ThumbnailTabHelper(content::WebContents* contents);
   friend class content::WebContentsUserData<ThumbnailTabHelper>;
 
-  enum class TriggerReason {
-    TAB_HIDDEN,
-    NAVIGATING_AWAY,
-  };
+  void UpdateCurrentUrl(const GURL& url);
+  void ScheduleThumbnailCapture(CaptureSchedule schedule);
+  void StartThumbnailCapture(CaptureSchedule schedule);
+  void ProcessCapturedThumbnail(const CaptureInfo& capture_info,
+                                base::TimeTicks start_time,
+                                const SkBitmap& bitmap);
+  void StoreThumbnail(const CaptureInfo& capture_info,
+                      base::TimeTicks start_time,
+                      ThumbnailImage thumbnail);
+  void NotifyTabPreviewChanged();
 
-  // Used for UMA histograms. Don't change or delete entries, and only add new
-  // ones at the end.
-  enum class Outcome {
-    SUCCESS = 0,
-    NOT_ATTEMPTED_PENDING_NAVIGATION,
-    NOT_ATTEMPTED_NO_PAINT_YET,
-    NOT_ATTEMPTED_IN_PROGRESS,
-    NOT_ATTEMPTED_NO_WEBCONTENTS,
-    NOT_ATTEMPTED_NO_URL,
-    NOT_ATTEMPTED_SHOULD_NOT_ACQUIRE,
-    NOT_ATTEMPTED_VIEW_NOT_AVAILABLE,
-    NOT_ATTEMPTED_EMPTY_RECT,
-    CANCELED,
-    READBACK_FAILED,
-    // Add new entries here!
-    COUNT
-  };
-
-  // content::RenderWidgetHostObserver overrides.
-  void RenderWidgetHostVisibilityChanged(content::RenderWidgetHost* widget_host,
-                                         bool became_visible) override;
-  void RenderWidgetHostDestroyed(
-      content::RenderWidgetHost* widget_host) override;
-
-  // content::WebContentsObserver overrides.
-  void RenderViewCreated(content::RenderViewHost* render_view_host) override;
-  void RenderViewHostChanged(content::RenderViewHost* old_host,
-                             content::RenderViewHost* new_host) override;
-  void RenderViewDeleted(content::RenderViewHost* render_view_host) override;
-  void DidStartNavigation(
-      content::NavigationHandle* navigation_handle) override;
-  void DidFinishNavigation(
-      content::NavigationHandle* navigation_handle) override;
-  void DocumentAvailableInMainFrame() override;
-  void DocumentOnLoadCompletedInMainFrame() override;
-  void DidFirstVisuallyNonEmptyPaint() override;
-  void DidStartLoading() override;
-  void NavigationStopped() override;
-
-  void StartWatchingRenderViewHost(content::RenderViewHost* render_view_host);
-  void StopWatchingRenderViewHost(content::RenderViewHost* render_view_host);
-
-  // Starts the process of capturing a thumbnail of the current tab contents if
-  // necessary and possible.
-  void StartThumbnailCaptureIfNecessary(TriggerReason trigger);
-
-  // Creates a thumbnail from the web contents bitmap.
-  void ProcessCapturedBitmap(TriggerReason trigger, const SkBitmap& bitmap);
-
-  // Called when the current tab gets hidden.
-  void TabHidden();
-
-  static void LogThumbnailingOutcome(TriggerReason trigger, Outcome outcome);
-
-  bool did_navigation_finish_ = false;
-  bool has_received_document_since_navigation_finished_ = false;
-  bool has_painted_since_document_received_ = false;
-
-  ui::PageTransition page_transition_ = ui::PAGE_TRANSITION_LINK;
-  bool load_interrupted_ = false;
-
-  bool thumbnailing_in_progress_ = false;
-  bool waiting_for_capture_ = false;
-
-  base::TimeTicks copy_from_surface_start_time_;
+  // For tabs in the process of loading, schedules another capture if none is
+  // currently queued.
+  void MaybeScheduleAnotherCapture(const CaptureInfo& capture_info,
+                                   base::TimeTicks finish_time);
 
   ThumbnailImage thumbnail_;
+  ThumbnailState thumbnail_state_ = ThumbnailState::kNoThumbnail;
+  GURL thumbnail_url_;
 
-  ScopedObserver<content::RenderWidgetHost, content::RenderWidgetHostObserver>
-      observer_{this};
+  // Caches whether or not the web contents view is visible. See notes in
+  // VisibilityChanged() for more information.
+  bool view_is_visible_;  // set in constructor
+  bool is_loading_ = false;
+  GURL current_url_;
+
+  // The time that the most recently-scheduled capture is/was scheduled for.
+  // Can be in the past. Used to prevent captures from bunching up or being
+  // scheduled in the wrong order.
+  base::TimeTicks last_scheduled_capture_time_;
 
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 
diff --git a/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.cc b/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.cc
new file mode 100644
index 0000000..35a75f4
--- /dev/null
+++ b/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.cc
@@ -0,0 +1,93 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.h"
+
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/render_view_host.h"
+#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/render_widget_host_view.h"
+#include "content/public/browser/web_contents.h"
+
+ThumbnailWebContentsObserver::ThumbnailWebContentsObserver(
+    content::WebContents* contents)
+    : content::WebContentsObserver(contents) {}
+
+ThumbnailWebContentsObserver::~ThumbnailWebContentsObserver() = default;
+
+void ThumbnailWebContentsObserver::OnVisibilityChanged(
+    content::Visibility visibility) {
+  VisibilityChanged(visibility == content::Visibility::VISIBLE);
+}
+
+void ThumbnailWebContentsObserver::DidStartNavigation(
+    content::NavigationHandle* navigation_handle) {
+  if (navigation_handle->IsInMainFrame() &&
+      !navigation_handle->IsSameDocument()) {
+    TopLevelNavigationStarted(
+        navigation_handle->GetWebContents()->GetVisibleURL());
+  }
+}
+
+void ThumbnailWebContentsObserver::DidRedirectNavigation(
+    content::NavigationHandle* navigation_handle) {
+  if (navigation_handle->IsInMainFrame() &&
+      !navigation_handle->IsSameDocument()) {
+    TopLevelNavigationStarted(
+        navigation_handle->GetWebContents()->GetVisibleURL());
+  }
+}
+
+void ThumbnailWebContentsObserver::DidFinishNavigation(
+    content::NavigationHandle* navigation_handle) {
+  if (navigation_handle->IsInMainFrame()) {
+    TopLevelNavigationEnded(
+        navigation_handle->GetWebContents()->GetVisibleURL());
+  }
+}
+
+void ThumbnailWebContentsObserver::DidStartLoading() {
+  PageLoadStarted(FrameContext::kMainFrame);
+}
+
+void ThumbnailWebContentsObserver::DidStopLoading() {
+  PageLoadFinished(FrameContext::kMainFrame);
+}
+
+void ThumbnailWebContentsObserver::DidFinishLoad(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& validated_url) {
+  PageLoadFinished(ContextFromRenderFrameHost(render_frame_host));
+}
+
+void ThumbnailWebContentsObserver::DidFailLoad(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& validated_url,
+    int error_code,
+    const base::string16& error_description) {
+  PageLoadFinished(ContextFromRenderFrameHost(render_frame_host));
+}
+
+void ThumbnailWebContentsObserver::MainFrameWasResized(bool width_changed) {
+  PageUpdated(FrameContext::kMainFrame);
+}
+
+void ThumbnailWebContentsObserver::FrameSizeChanged(
+    content::RenderFrameHost* render_frame_host,
+    const gfx::Size& frame_size) {
+  PageUpdated(ContextFromRenderFrameHost(render_frame_host));
+}
+
+void ThumbnailWebContentsObserver::NavigationStopped() {
+  TopLevelNavigationEnded(web_contents()->GetVisibleURL());
+  PageLoadFinished(FrameContext::kMainFrame);
+}
+
+// static
+ThumbnailWebContentsObserver::FrameContext
+ThumbnailWebContentsObserver::ContextFromRenderFrameHost(
+    content::RenderFrameHost* render_frame_host) {
+  return render_frame_host->GetParent() ? FrameContext::kChildFrame
+                                        : FrameContext::kMainFrame;
+}
diff --git a/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.h b/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.h
new file mode 100644
index 0000000..285fe1e
--- /dev/null
+++ b/chrome/browser/ui/thumbnails/thumbnail_web_contents_observer.h
@@ -0,0 +1,81 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_WEB_CONTENTS_OBSERVER_H_
+#define CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_WEB_CONTENTS_OBSERVER_H_
+
+#include "base/macros.h"
+#include "base/scoped_observer.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "url/gurl.h"
+
+namespace content {
+class NavigationHandle;
+class RenderFrameHost;
+}  // namespace content
+
+// Base class for thumbnail tab helper; processes specific web contents events
+// into a filtered-down set of navigation and loading events for ease of
+// processing.
+class ThumbnailWebContentsObserver : public content::WebContentsObserver {
+ public:
+  ~ThumbnailWebContentsObserver() override;
+
+ protected:
+  enum class FrameContext { kMainFrame, kChildFrame };
+
+  explicit ThumbnailWebContentsObserver(content::WebContents* contents);
+
+  // Called when navigation in the top-level browser window starts.
+  virtual void TopLevelNavigationStarted(const GURL& url) = 0;
+  // Called when navigation in the top-level browser window completes.
+  virtual void TopLevelNavigationEnded(const GURL& url) = 0;
+  // Called when the page/tab's visibility changes.
+  // If |view_is_valid| is false, no attempt should be made to read from the
+  // contents pane.
+  virtual void VisibilityChanged(bool visible) = 0;
+  // Called when a page begins to load.
+  virtual void PageLoadStarted(FrameContext frame_context) = 0;
+  // Called when a page finishes loading.
+  virtual void PageLoadFinished(FrameContext frame_context) = 0;
+  // Called when the page is resized or otherwise updated.
+  virtual void PageUpdated(FrameContext frame_context) = 0;
+
+  void OnVisibilityChanged(content::Visibility visibility) override;
+
+  // Track when navigation happens so that we know when a thumbnail is no longer
+  // valid (thumbnail will still be valid for the old URL until the new
+  // navigation has committed and the new page render occurs).
+  void DidStartNavigation(
+      content::NavigationHandle* navigation_handle) override;
+  void DidRedirectNavigation(
+      content::NavigationHandle* navigation_handle) override;
+  void DidFinishNavigation(
+      content::NavigationHandle* navigation_handle) override;
+
+  // Track the progress of loading. Thumbnails should be captured during the
+  // loading process, since some pages take a long time to load, but there is no
+  // point to capturing a thumbnail of a page that has not rendered anything
+  // yet.
+  void DidStartLoading() override;
+  void DidStopLoading() override;
+  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
+                     const GURL& validated_url) override;
+  void DidFailLoad(content::RenderFrameHost* render_frame_host,
+                   const GURL& validated_url,
+                   int error_code,
+                   const base::string16& error_description) override;
+  void NavigationStopped() override;
+  void MainFrameWasResized(bool width_changed) override;
+  void FrameSizeChanged(content::RenderFrameHost* render_frame_host,
+                        const gfx::Size& frame_size) override;
+
+ private:
+  static FrameContext ContextFromRenderFrameHost(
+      content::RenderFrameHost* render_frame_host);
+
+  DISALLOW_COPY_AND_ASSIGN(ThumbnailWebContentsObserver);
+};
+
+#endif  // CHROME_BROWSER_UI_THUMBNAILS_THUMBNAIL_WEB_CONTENTS_OBSERVER_H_