Add FIRST_CONTENTFUL_PAINT message for NavigationListener

In addition to PAGE_LOAD_END & DOM_CONTENT_LOADED, which are
associated with the `load` and `domcontentloaded` events in JS,
users of NavigationListener wants to listen to when the
first contentful paint happens as well. This CL adds the
event by listening to the FCP event, newly plumbed from the
renderer.

Bug: 332809183
Change-Id: I0195c03fea0fc1a1ec1d7e35debcce73bfa7a9f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5992952
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Reviewed-by: Richard (Torne) Coles <torne@chromium.org>
Commit-Queue: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Colin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1381036}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/NavigationListenerTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/NavigationListenerTest.java
index b713e56..0b22521 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/NavigationListenerTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/NavigationListenerTest.java
@@ -1162,6 +1162,16 @@
             data = mListener.waitForOnPostMessage();
             assertNavigationMessageType(data, "PAGE_LOAD_END");
             Assert.assertEquals(currentPageReplyProxy, data.mReplyProxy);
+
+            // FIRST_CONTENTFUL_PAINT is only invoked on pages that have non-empty content. All of
+            // about:blank, PAGE_B, and PAGE_WITH_IFRAME are empty/background-color-only.
+            if (!url.equals("about:blank")
+                    && !url.equals(mTestServer.getURL(PAGE_B))
+                    && !url.equals(mTestServer.getURL(PAGE_WITH_IFRAME))) {
+                data = mListener.waitForOnPostMessage();
+                assertNavigationMessageType(data, "FIRST_CONTENTFUL_PAINT");
+                Assert.assertEquals(currentPageReplyProxy, data.mReplyProxy);
+            }
         }
 
         return currentPageReplyProxy;
diff --git a/components/js_injection/browser/navigation_listener_browsertest.cc b/components/js_injection/browser/navigation_listener_browsertest.cc
index de25ed3..3dbc4f1 100644
--- a/components/js_injection/browser/navigation_listener_browsertest.cc
+++ b/components/js_injection/browser/navigation_listener_browsertest.cc
@@ -243,6 +243,7 @@
       expected_dict.Set("supports_start_and_redirect", true);
       expected_dict.Set("supports_history_details", true);
       expected_dict.Set("supports_dom_content_loaded", true);
+      expected_dict.Set("supports_first_contentful_paint", true);
     }
     ASSERT_EQ(
         NavigationWebMessageSender::CreateWebMessage(std::move(expected_dict))
diff --git a/components/js_injection/browser/navigation_web_message_sender.cc b/components/js_injection/browser/navigation_web_message_sender.cc
index a074914e..2e90b79c 100644
--- a/components/js_injection/browser/navigation_web_message_sender.cc
+++ b/components/js_injection/browser/navigation_web_message_sender.cc
@@ -184,6 +184,8 @@
 const char NavigationWebMessageSender::kPageLoadEndMessage[] = "PAGE_LOAD_END";
 const char NavigationWebMessageSender::kDOMContentLoadedMessage[] =
     "DOM_CONTENT_LOADED";
+const char NavigationWebMessageSender::kFirstContentfulPaintMessage[] =
+    "FIRST_CONTENTFUL_PAINT";
 const char NavigationWebMessageSender::kPageDeletedMessage[] = "PAGE_DELETED";
 
 // static
@@ -243,7 +245,8 @@
           .Set("type", kOptedInMessage)
           .Set("supports_start_and_redirect", true)
           .Set("supports_history_details", true)
-          .Set("supports_dom_content_loaded", true);
+          .Set("supports_dom_content_loaded", true)
+          .Set("supports_first_contentful_paint", true);
   PostMessage(std::move(message_dict));
 }
 
@@ -264,6 +267,13 @@
   PostMessageWithType(kPageLoadEndMessage);
 }
 
+void NavigationWebMessageSender::OnFirstContentfulPaintInPrimaryMainFrame() {
+  if (!page().IsPrimary()) {
+    return;
+  }
+  PostMessageWithType(kFirstContentfulPaintMessage);
+}
+
 bool NavigationWebMessageSender::ShouldSendMessageForNavigation(
     content::NavigationHandle* navigation_handle) {
   // Only send navigation notifications for primary pages, and only from the
diff --git a/components/js_injection/browser/navigation_web_message_sender.h b/components/js_injection/browser/navigation_web_message_sender.h
index a3a0db3d..6c4a3f4 100644
--- a/components/js_injection/browser/navigation_web_message_sender.h
+++ b/components/js_injection/browser/navigation_web_message_sender.h
@@ -86,6 +86,10 @@
   // dispatched on `DOMContentLoaded()`.
   static const char kDOMContentLoadedMessage[];
 
+  // Indicates that the primary main frame just did a first contentful paint.
+  // This is dispatched on `OnFirstContentfulPaintInPrimaryMainFrame()`.
+  static const char kFirstContentfulPaintMessage[];
+
   // Indicates that the page has been deleted. This is dispatched from the class
   // destructor, since this is a PageUserData. If the page is BFCached, this
   // will be when the page is evicted. Otherwise, it will be when the primary
@@ -130,6 +134,7 @@
       content::NavigationHandle* navigation_handle) override;
   void DidFinishNavigation(
       content::NavigationHandle* navigation_handle) override;
+  void OnFirstContentfulPaintInPrimaryMainFrame() override;
 
   void PostMessageWithType(std::string_view type);
   void PostMessage(base::Value::Dict message_dict);
diff --git a/content/browser/renderer_host/page_impl.cc b/content/browser/renderer_host/page_impl.cc
index 7eb398b..7a943b8 100644
--- a/content/browser/renderer_host/page_impl.cc
+++ b/content/browser/renderer_host/page_impl.cc
@@ -279,6 +279,10 @@
   if (is_on_load_completed_in_main_document())
     main_document_->DocumentOnLoadCompleted();
 
+  if (did_first_contentful_paint_in_main_document()) {
+    main_document_->OnFirstContentfulPaint();
+  }
+
   main_document_->ForEachRenderFrameHost(
       &RenderFrameHostImpl::MaybeDispatchDidFinishLoadOnPrerenderActivation);
 }
diff --git a/content/browser/renderer_host/page_impl.h b/content/browser/renderer_host/page_impl.h
index 0a60320..ab800be 100644
--- a/content/browser/renderer_host/page_impl.h
+++ b/content/browser/renderer_host/page_impl.h
@@ -90,6 +90,13 @@
     is_on_load_completed_in_main_document_ = completed;
   }
 
+  bool did_first_contentful_paint_in_main_document() const {
+    return did_first_contentful_paint_in_main_document_;
+  }
+  void set_did_first_contentful_paint_in_main_document() {
+    did_first_contentful_paint_in_main_document_ = true;
+  }
+
   bool is_main_document_element_available() const {
     return is_main_document_element_available_;
   }
@@ -289,6 +296,9 @@
   // run for the main document.
   bool is_on_load_completed_in_main_document_ = false;
 
+  // True if the main document had done a first contentful paint.
+  bool did_first_contentful_paint_in_main_document_ = false;
+
   // True if we've received a notification that the window.document element
   // became available for the main document.
   bool is_main_document_element_available_ = false;
diff --git a/content/browser/renderer_host/render_frame_host_delegate.h b/content/browser/renderer_host/render_frame_host_delegate.h
index d7eb6e3..1b88c893 100644
--- a/content/browser/renderer_host/render_frame_host_delegate.h
+++ b/content/browser/renderer_host/render_frame_host_delegate.h
@@ -759,6 +759,9 @@
   // See https://explainers-by-googlers.github.io/partitioned-popins/
   virtual WebContents* GetOpenedPartitionedPopin() const;
 
+  // Called when a first contentful paint happened in the primary main frame.
+  virtual void OnFirstContentfulPaintInPrimaryMainFrame() {}
+
  protected:
   virtual ~RenderFrameHostDelegate() = default;
 };
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index d29dac8..95b2ad6 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -7455,6 +7455,15 @@
   }
 }
 
+void RenderFrameHostImpl::OnFirstContentfulPaint() {
+  GetPage().set_did_first_contentful_paint_in_main_document();
+  if (IsInPrimaryMainFrame()) {
+    // Notify the delegates of the FCP. Note that the notifications for
+    // prerendering pages will be deferred until activation.
+    delegate_->OnFirstContentfulPaintInPrimaryMainFrame();
+  }
+}
+
 void RenderFrameHostImpl::UpdateEncoding(const std::string& encoding_name) {
   if (!is_main_frame()) {
     mojo::ReportBadMessage("Renderer sent updated encoding for a subframe.");
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index 7029062..940c999 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -2569,6 +2569,7 @@
   void DraggableRegionsChanged(
       std::vector<blink::mojom::DraggableRegionPtr> regions) override;
   void NotifyDocumentInteractive() override;
+  void OnFirstContentfulPaint() override;
 
   void ReportNoBinderForInterface(const std::string& error);
 
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index d449d20..05903ee 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -6980,6 +6980,11 @@
   GetDelegate()->DraggableRegionsChanged(regions, this);
 }
 
+void WebContentsImpl::OnFirstContentfulPaintInPrimaryMainFrame() {
+  observers_.NotifyObservers(
+      &WebContentsObserver::OnFirstContentfulPaintInPrimaryMainFrame);
+}
+
 void WebContentsImpl::NotifyChangedNavigationState(
     InvalidateTypes changed_flags) {
   NotifyNavigationStateChanged(changed_flags);
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 080420ea..0e0cb85 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -917,6 +917,7 @@
                             int error_code) override;
   void DraggableRegionsChanged(
       const std::vector<blink::mojom::DraggableRegionPtr>& regions) override;
+  void OnFirstContentfulPaintInPrimaryMainFrame() override;
 
   // RenderViewHostDelegate ----------------------------------------------------
   RenderViewHostDelegateView* GetDelegateView() override;
diff --git a/content/public/browser/web_contents_observer.h b/content/public/browser/web_contents_observer.h
index 0379a5c..f3284b1 100644
--- a/content/public/browser/web_contents_observer.h
+++ b/content/public/browser/web_contents_observer.h
@@ -972,6 +972,9 @@
   // Called when WebContents received a request to vibrate the page.
   virtual void VibrationRequested() {}
 
+  // Called when a first contentful paint happened in the primary main frame.
+  virtual void OnFirstContentfulPaintInPrimaryMainFrame() {}
+
   WebContents* web_contents() const;
 
  protected:
diff --git a/third_party/blink/public/mojom/frame/frame.mojom b/third_party/blink/public/mojom/frame/frame.mojom
index 75e6669..5e71f0ca 100644
--- a/third_party/blink/public/mojom/frame/frame.mojom
+++ b/third_party/blink/public/mojom/frame/frame.mojom
@@ -1375,4 +1375,7 @@
   // Notify the browser that the draggable regions defined by the app-region
   // CSS property have been updated.
   DraggableRegionsChanged(array<DraggableRegion> regions);
+
+  // Called when the main frame did a first contentful paint.
+  OnFirstContentfulPaint();
 };
diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc
index f5fe2e06..ad134361 100644
--- a/third_party/blink/renderer/core/exported/web_view_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_view_impl.cc
@@ -1214,6 +1214,10 @@
   local_main_frame_host_remote_->DidFirstVisuallyNonEmptyPaint();
 }
 
+void WebViewImpl::OnFirstContentfulPaint() {
+  local_main_frame_host_remote_->OnFirstContentfulPaint();
+}
+
 void WebViewImpl::UpdateICBAndResizeViewport(
     const gfx::Size& visible_viewport_size) {
   // We'll keep the initial containing block size from changing when the top
diff --git a/third_party/blink/renderer/core/exported/web_view_impl.h b/third_party/blink/renderer/core/exported/web_view_impl.h
index d5397c5..e4784453 100644
--- a/third_party/blink/renderer/core/exported/web_view_impl.h
+++ b/third_party/blink/renderer/core/exported/web_view_impl.h
@@ -628,6 +628,9 @@
   // words, after the frame has painted something.
   void DidFirstVisuallyNonEmptyPaint();
 
+  // Caleld once the first contentful paint happens on the main frame.
+  void OnFirstContentfulPaint();
+
   scheduler::WebAgentGroupScheduler& GetWebAgentGroupScheduler();
 
   // Returns true if the page supports app-region: drag/no-drag.
diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc
index 5c8c50f..6a071e6 100644
--- a/third_party/blink/renderer/core/frame/local_frame.cc
+++ b/third_party/blink/renderer/core/frame/local_frame.cc
@@ -991,6 +991,12 @@
   }
 }
 
+void LocalFrame::OnFirstContentfulPaint() {
+  if (IsOutermostMainFrame()) {
+    GetPage()->GetChromeClient().OnFirstContentfulPaint();
+  }
+}
+
 bool LocalFrame::CanAccessEvent(
     const WebInputEventAttribution& attribution) const {
   switch (attribution.type()) {
diff --git a/third_party/blink/renderer/core/frame/local_frame.h b/third_party/blink/renderer/core/frame/local_frame.h
index 13d1bea..7e65f92 100644
--- a/third_party/blink/renderer/core/frame/local_frame.h
+++ b/third_party/blink/renderer/core/frame/local_frame.h
@@ -850,6 +850,9 @@
   // to FrameFirstPaint.
   void OnFirstPaint(bool text_painted, bool image_painted);
 
+  // Invoked on first contentful paint on this frame.
+  void OnFirstContentfulPaint();
+
 #if BUILDFLAG(IS_MAC)
   void ResetTextInputHostForTesting();
   void RebindTextInputHostForTesting();
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index 3979c39..0802173 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -7331,6 +7331,7 @@
   }
   void DraggableRegionsChanged(
       Vector<mojom::blink::DraggableRegionPtr> regions) override {}
+  void OnFirstContentfulPaint() override {}
 
   // !!!!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!!!!
   // If the actual counts in the tests below increase, this could be an
diff --git a/third_party/blink/renderer/core/page/chrome_client.h b/third_party/blink/renderer/core/page/chrome_client.h
index 4d77c94..7843e55 100644
--- a/third_party/blink/renderer/core/page/chrome_client.h
+++ b/third_party/blink/renderer/core/page/chrome_client.h
@@ -598,6 +598,8 @@
 
   virtual float ZoomFactorForViewportLayout() { return 1; }
 
+  virtual void OnFirstContentfulPaint() {}
+
  protected:
   ChromeClient() = default;
 
diff --git a/third_party/blink/renderer/core/page/chrome_client_impl.cc b/third_party/blink/renderer/core/page/chrome_client_impl.cc
index 93260285..d07cb09 100644
--- a/third_party/blink/renderer/core/page/chrome_client_impl.cc
+++ b/third_party/blink/renderer/core/page/chrome_client_impl.cc
@@ -1502,4 +1502,8 @@
   return window;
 }
 
+void ChromeClientImpl::OnFirstContentfulPaint() {
+  web_view_->OnFirstContentfulPaint();
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/page/chrome_client_impl.h b/third_party/blink/renderer/core/page/chrome_client_impl.h
index b099f2e..c7634f2e 100644
--- a/third_party/blink/renderer/core/page/chrome_client_impl.h
+++ b/third_party/blink/renderer/core/page/chrome_client_impl.h
@@ -314,6 +314,8 @@
 
   float ZoomFactorForViewportLayout() override;
 
+  void OnFirstContentfulPaint() override;
+
  private:
   bool IsChromeClientImpl() const override { return true; }
 
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing.cc b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
index 269bb47b..3dc313f 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
@@ -445,8 +445,10 @@
     soft_navigation_fcp_reported_ = true;
     return;
   }
-  if (GetFrame())
+  if (GetFrame()) {
+    GetFrame()->OnFirstContentfulPaint();
     GetFrame()->Loader().Progress().DidFirstContentfulPaint();
+  }
   NotifyPaintTimingChanged();
   fmp_detector_->NotifyFirstContentfulPaint(
       paint_details_.first_contentful_paint_presentation_);