prerender: Resending prerender activation for DevTools.

This CL stores prerender activation in WebContents and resend it
once DevTools is reopened as a workaround to the current unsupported
prerender activation in DevTools.

Design doc: https://docs.google.com/document/d/1l5S3DCMVsFfF_Thg95BT9292sDxas1wzKALz8J9FrB8/edit?usp=sharing

Change-Id: Ib2997325e73a7fbf9a0bfde0228006624d931c22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3805129
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Commit-Queue: Huanpo Lin <robertlin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1041841}
diff --git a/content/browser/devtools/devtools_instrumentation.cc b/content/browser/devtools/devtools_instrumentation.cc
index 19cc663..919c3245 100644
--- a/content/browser/devtools/devtools_instrumentation.cc
+++ b/content/browser/devtools/devtools_instrumentation.cc
@@ -318,6 +318,11 @@
 
 void DidActivatePrerender(const NavigationRequest& nav_request) {
   FrameTreeNode* ftn = nav_request.frame_tree_node();
+  WebContentsImpl* web_contents = WebContentsImpl::FromFrameTreeNode(ftn);
+  // Record prerender activation here because users don't necessarily open
+  // DevTools when the activation is triggered. If the DevTools is not opened at
+  // the moment, recording the activation here will still preserve the signal.
+  web_contents->set_last_navigation_was_prerender_activation_for_devtools();
   DispatchToAgents(ftn, &protocol::PageHandler::DidActivatePrerender,
                    nav_request);
 }
diff --git a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
index acf3ede..34bf2fe 100644
--- a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
+++ b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
@@ -223,9 +223,18 @@
         prerender_helper_->GetPrerenderedMainFrameHost(host_id));
   }
 
- private:
+  void NavigatePrimaryPage(const GURL& url) {
+    prerender_helper_->NavigatePrimaryPage(url);
+  }
+
+  // WebContentsDelegate overrides.
+  bool IsPrerender2Supported(WebContents& web_contents) override {
+    return true;
+  }
+
   WebContents* web_contents() const { return shell()->web_contents(); }
 
+ private:
   std::unique_ptr<test::PrerenderTestHelper> prerender_helper_;
 };
 
@@ -3387,4 +3396,63 @@
       PrerenderHost::FinalStatus::kMojoBinderPolicy, 1);
 }
 
+IN_PROC_BROWSER_TEST_F(PrerenderDevToolsProtocolTest,
+                       RemoveStoredPrerenderActivationIfNavigateAway) {
+  base::HistogramTester histogram_tester;
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL kInitialUrl = GetUrl("/empty.html");
+  const GURL kPrerenderingUrl = GetUrl("/empty.html?prerender");
+  WebContentsImpl* web_contents_impl =
+      static_cast<WebContentsImpl*>(web_contents());
+
+  // Navigate to an initial page.
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Make a prerendered page.
+  AddPrerender(kPrerenderingUrl);
+
+  Attach();
+  SendCommandSync("Page.enable");
+  SendCommandSync("Runtime.enable");
+  NavigatePrimaryPage(kPrerenderingUrl);
+
+  WaitForNotification("Page.prerenderAttemptCompleted", true);
+
+  // Navigate away from the prerendered page, and this should trigger the
+  // mechanism of removing the stored prerender activation.
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+  ASSERT_FALSE(web_contents_impl
+                   ->last_navigation_was_prerender_activation_for_devtools());
+}
+
+IN_PROC_BROWSER_TEST_F(PrerenderDevToolsProtocolTest,
+                       NewPrerenderActivationOverrideTheOldOne) {
+  base::HistogramTester histogram_tester;
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL kInitialUrl = GetUrl("/empty.html");
+  const GURL kPrerenderingUrl = GetUrl("/empty.html?prerender");
+  const GURL kPrerenderingUrl2 = GetUrl("/title1.html?prerender2");
+  WebContentsImpl* web_contents_impl =
+      static_cast<WebContentsImpl*>(web_contents());
+
+  // Navigate to an initial page.
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Make a prerendered page.
+  AddPrerender(kPrerenderingUrl);
+
+  Attach();
+  SendCommandSync("Page.enable");
+  SendCommandSync("Runtime.enable");
+  NavigatePrimaryPage(kPrerenderingUrl);
+
+  WaitForNotification("Page.prerenderAttemptCompleted", true);
+
+  // Trigger another prerender activation.
+  AddPrerender(kPrerenderingUrl2);
+  NavigatePrimaryPage(kPrerenderingUrl2);
+  ASSERT_TRUE(web_contents_impl
+                  ->last_navigation_was_prerender_activation_for_devtools());
+}
+
 }  // namespace content
diff --git a/content/browser/devtools/protocol/page_handler.cc b/content/browser/devtools/protocol/page_handler.cc
index c5017a4..090ab72 100644
--- a/content/browser/devtools/protocol/page_handler.cc
+++ b/content/browser/devtools/protocol/page_handler.cc
@@ -346,6 +346,7 @@
 
 Response PageHandler::Enable() {
   enabled_ = true;
+  RetrievePrerenderActivationFromWebContents();
   return Response::FallThrough();
 }
 
@@ -1939,6 +1940,7 @@
 }
 
 void PageHandler::DidActivatePrerender(const NavigationRequest& nav_request) {
+  has_dispatched_stored_prerender_activation_ = false;
   if (!enabled_)
     return;
   FrameTreeNode* ftn = nav_request.frame_tree_node();
@@ -1953,6 +1955,7 @@
                                      const std::string& initiating_frame_id,
                                      PrerenderHost::FinalStatus status,
                                      const std::string& reason_details) {
+  has_dispatched_stored_prerender_activation_ = false;
   if (!enabled_)
     return;
   DCHECK_NE(status, PrerenderHost::FinalStatus::kActivated);
@@ -1968,5 +1971,21 @@
   return enabled_ && bypass_csp_;
 }
 
+void PageHandler::RetrievePrerenderActivationFromWebContents() {
+  if (!host_)
+    return;
+  WebContentsImpl* web_contents =
+      WebContentsImpl::FromRenderFrameHostImpl(host_);
+  if (web_contents->last_navigation_was_prerender_activation_for_devtools() &&
+      !has_dispatched_stored_prerender_activation_) {
+    std::string frame_token =
+        host_->frame_tree_node()->devtools_frame_token().ToString();
+    has_dispatched_stored_prerender_activation_ = true;
+    frontend_->PrerenderAttemptCompleted(
+        frame_token, host_->GetLastCommittedURL().spec(),
+        Page::PrerenderFinalStatusEnum::Activated);
+  }
+}
+
 }  // namespace protocol
 }  // namespace content
diff --git a/content/browser/devtools/protocol/page_handler.h b/content/browser/devtools/protocol/page_handler.h
index 2a263de..3e38acc 100644
--- a/content/browser/devtools/protocol/page_handler.h
+++ b/content/browser/devtools/protocol/page_handler.h
@@ -223,6 +223,8 @@
   using ResponseOrWebContents = absl::variant<Response, WebContentsImpl*>;
   ResponseOrWebContents GetWebContentsForTopLevelActiveFrame();
 
+  void RetrievePrerenderActivationFromWebContents();
+
   const bool allow_unsafe_operations_;
   const bool is_trusted_;
   const absl::optional<url::Origin> navigation_initiator_origin_;
@@ -241,6 +243,10 @@
   int frame_counter_;
   int frames_in_flight_;
 
+  // Whether stored prerender activation has been dispatched to Devtools. Reset
+  // whenever a new prerender event received.
+  bool has_dispatched_stored_prerender_activation_ = false;
+
   // |video_consumer_| consumes video frames from FrameSinkVideoCapturerImpl,
   // and provides PageHandler with these frames via OnFrameFromVideoConsumer.
   // This is only used if Viz is enabled and if OS is not Android.
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 29e19a8..8f0afba 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -5717,6 +5717,15 @@
       was_ever_audible_ = false;
     }
 
+    // Clear the stored prerender activation result if this is not a prerender
+    // activation. If this is another prerender activation, it will override
+    // the old result in DevTools.
+    if (!navigation_handle->IsPrerenderedPageActivation() &&
+        !navigation_handle->IsSameDocument() &&
+        navigation_handle->IsInPrimaryMainFrame()) {
+      last_navigation_was_prerender_activation_for_devtools_ = false;
+    }
+
     if (!navigation_handle->IsSameDocument())
       last_screen_orientation_change_time_ = base::TimeTicks();
   }
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 335ed04c..345eba7 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -1344,6 +1344,16 @@
     return mouse_lock_widget_;
   }
 
+  // Record a prerender activation for DevTools.
+  void set_last_navigation_was_prerender_activation_for_devtools() {
+    last_navigation_was_prerender_activation_for_devtools_ = true;
+  }
+
+  // Check if prerender was just activated.
+  bool last_navigation_was_prerender_activation_for_devtools() {
+    return last_navigation_was_prerender_activation_for_devtools_;
+  }
+
  private:
   using FrameTreeIterationCallback = base::RepeatingCallback<void(FrameTree*)>;
   using RenderViewHostIterationCallback =
@@ -2349,6 +2359,12 @@
 
   VisibleTimeRequestTrigger visible_time_request_trigger_;
 
+  // Stores the information whether last navigation was prerender activation for
+  // DevTools. Set when a prerender activation completes, and cleared when
+  // either DevTools is opened and consults this value or when a non-prerendered
+  // navigation commits in the primary main frame.
+  bool last_navigation_was_prerender_activation_for_devtools_ = false;
+
   bool prerender2_disabled_ = false;
 
   base::WeakPtrFactory<WebContentsImpl> loading_weak_factory_{this};
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation-expected.txt
new file mode 100644
index 0000000..549faa6
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation-expected.txt
@@ -0,0 +1,20 @@
+Test that prerender navigations report the resent activation.
+{
+    method : Page.prerenderAttemptCompleted
+    params : {
+        finalStatus : Activated
+        initiatingFrameId : <string>
+        prerenderingUrl : http://127.0.0.1:8000/inspector-protocol/prerender/resources/empty.html
+    }
+    sessionId : <string>
+}
+{
+    method : Page.prerenderAttemptCompleted
+    params : {
+        finalStatus : Activated
+        initiatingFrameId : <string>
+        prerenderingUrl : http://127.0.0.1:8000/inspector-protocol/prerender/resources/empty.html
+    }
+    sessionId : <string>
+}
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation.js b/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation.js
new file mode 100644
index 0000000..187596d
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/prerender/report-prerender-resent-activation.js
@@ -0,0 +1,17 @@
+(async function(testRunner) {
+  const {page, session, dp} = await testRunner.startBlank(
+      `Test that prerender navigations report the resent activation.`);
+  await dp.Page.enable();
+
+  // Navigate to speculation rules Prerender Page.
+  await page.navigate('resources/simple-prerender.html');
+  session.evaluate(`document.getElementById('link').click()`);
+  const statusReport = await dp.Page.oncePrerenderAttemptCompleted();
+  testRunner.log(statusReport, '', ['initiatingFrameId', 'sessionId']);
+  await dp.Page.disable();
+  dp.Page.enable();
+  const resentStatusReport = await dp.Page.oncePrerenderAttemptCompleted();
+  testRunner.log(resentStatusReport, '', ['initiatingFrameId', 'sessionId']);
+
+  testRunner.completeTest();
+});