blob: e688714f76801d3fdaba8da59f83e1f8f7928d2c [file] [log] [blame]
Iman Saboori86e1b6d2023-08-28 18:42:351// Copyright 2023 The Chromium Authors
Iman Saboori64dc4de2022-11-29 18:58:062// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/browser/preloading/preloading_decider.h"
6
7#include <vector>
8
Takashi Nakayamae234c492025-08-04 03:16:489#include "base/feature_list.h"
Iman Saboorieda025c72023-05-12 18:55:2910#include "base/strings/strcat.h"
Devon Loehrc0138d8d2025-03-04 01:11:4211#include "base/strings/to_string.h"
Iman Saboori64dc4de2022-11-29 18:58:0612#include "base/test/scoped_feature_list.h"
13#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
14#include "content/browser/preloading/prefetch/prefetch_features.h"
Hiroshige Hayashizakif55280f62025-08-19 17:23:2015#include "content/browser/preloading/prefetch/prefetch_request.h"
Hiroshige Hayashizaki4c3aa4742025-03-14 05:13:4916#include "content/browser/preloading/prefetch/prefetch_test_util_internal.h"
Iman Saboori64dc4de2022-11-29 18:58:0617#include "content/browser/preloading/prefetcher.h"
Kevin McNee98e068a2024-04-09 20:12:3418#include "content/browser/preloading/preloading.h"
Kevin McNee842eb0a2024-04-11 20:14:1619#include "content/browser/preloading/preloading_confidence.h"
Iman Saboorieda025c72023-05-12 18:55:2920#include "content/browser/preloading/preloading_data_impl.h"
Kevin McNee98e068a2024-04-09 20:12:3421#include "content/browser/preloading/preloading_trigger_type_impl.h"
Iman Saboori31bed872022-12-09 15:41:3122#include "content/browser/preloading/prerenderer.h"
Arthur Sonzognibdeca8e2023-09-11 08:32:1223#include "content/common/features.h"
Iman Saboori64dc4de2022-11-29 18:58:0624#include "content/public/browser/anchor_element_preconnect_delegate.h"
25#include "content/public/common/content_client.h"
Iman Saboori31f09492023-06-26 20:15:2026#include "content/public/test/mock_navigation_handle.h"
Kevin McNee031e28c2024-03-21 20:55:0027#include "content/public/test/navigation_simulator.h"
Iman Saboori31bed872022-12-09 15:41:3128#include "content/public/test/prerender_test_util.h"
Iman Saboori64dc4de2022-11-29 18:58:0629#include "content/public/test/test_browser_context.h"
30#include "content/public/test/test_renderer_host.h"
31#include "content/test/test_content_browser_client.h"
32#include "content/test/test_web_contents.h"
33#include "testing/gtest/include/gtest/gtest.h"
Adithya Srinivasan7239d4f2024-11-05 15:35:5334#include "third_party/blink/public/common/features.h"
Iman Saboori50d51092023-06-09 22:17:3035#include "third_party/blink/public/mojom/preloading/anchor_element_interaction_host.mojom.h"
Takashi Nakayamae234c492025-08-04 03:16:4836#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom-data-view.h"
Jeremy Roman2cf0e772023-03-13 23:20:0037#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom-shared.h"
Iman Saboori64dc4de2022-11-29 18:58:0638
39namespace content {
40namespace {
41
42class MockAnchorElementPreconnector : public AnchorElementPreconnectDelegate {
43 public:
Johanna9fe85f2023-01-17 10:15:4344 explicit MockAnchorElementPreconnector(RenderFrameHost& render_frame_host) {}
Iman Saboori64dc4de2022-11-29 18:58:0645 ~MockAnchorElementPreconnector() override = default;
46
47 void MaybePreconnect(const GURL& target) override { target_ = target; }
Arthur Sonzognic686e8f2024-01-11 08:36:3748 std::optional<GURL>& Target() { return target_; }
Iman Saboori64dc4de2022-11-29 18:58:0649
50 private:
Arthur Sonzognic686e8f2024-01-11 08:36:3751 std::optional<GURL> target_;
Iman Saboori64dc4de2022-11-29 18:58:0652};
53
Iman Saboori31bed872022-12-09 15:41:3154class MockPrerenderer : public Prerenderer {
55 public:
56 ~MockPrerenderer() override = default;
57
58 void ProcessCandidatesForPrerender(
59 const std::vector<blink::mojom::SpeculationCandidatePtr>& candidates)
60 override {
61 for (const auto& candidate : candidates) {
Takashi Nakayama978f0a152025-06-17 08:26:2562 // Immediate candidates are enacted by the same predictor that creates
63 // them.
Kevin McNee98e068a2024-04-09 20:12:3464 PreloadingTriggerType trigger_type =
65 PreloadingTriggerTypeFromSpeculationInjectionType(
66 candidate->injection_type);
67 PreloadingPredictor enacting_predictor =
68 GetPredictorForPreloadingTriggerType(trigger_type);
Kevin McNee842eb0a2024-04-11 20:14:1669 MaybePrerender(candidate, enacting_predictor, PreloadingConfidence{100});
Iman Saboori31bed872022-12-09 15:41:3170 }
71 }
72
Yoichi Osatofed56ff82024-02-29 01:13:4573 void OnLCPPredicted() override {}
74
Kevin McNee98e068a2024-04-09 20:12:3475 bool MaybePrerender(const blink::mojom::SpeculationCandidatePtr& candidate,
Kevin McNee842eb0a2024-04-11 20:14:1676 const PreloadingPredictor& enacting_predictor,
77 PreloadingConfidence confidence) override {
Taiyo Mizuhashif3098ea2023-10-11 13:14:2678 if (PrerenderExists(candidate->url)) {
79 return false;
80 }
81 prerenders_.emplace_back(candidate->url, candidate->eagerness);
82 return true;
Iman Saboori31bed872022-12-09 15:41:3183 }
84
85 bool ShouldWaitForPrerenderResult(const GURL& url) override {
Taiyo Mizuhashif3098ea2023-10-11 13:14:2686 return PrerenderExists(url);
Iman Saboori31bed872022-12-09 15:41:3187 }
88
Taiyo Mizuhashi295ccbdf2023-09-13 09:17:0289 void SetPrerenderCancellationCallback(
Taiyo Mizuhashif3098ea2023-10-11 13:14:2690 PrerenderCancellationCallback callback) override {
91 prerender_cancellation_callback_ = std::move(callback);
92 }
Taiyo Mizuhashi295ccbdf2023-09-13 09:17:0293
Taiyo Mizuhashif3098ea2023-10-11 13:14:2694 void OnCancel(size_t index) {
95 ASSERT_LT(index, prerenders_.size());
96 const auto& [url, _] = prerenders_[index];
97 prerender_cancellation_callback_.Run(url);
98 prerenders_.erase(prerenders_.begin() + index);
99 }
100
101 bool PrerenderExists(const GURL& url) {
102 return std::find_if(prerenders_.begin(), prerenders_.end(),
103 [&](const auto& prerender) {
104 return url == prerender.first;
105 }) != prerenders_.end();
106 }
107
108 std::vector<std::pair<GURL, blink::mojom::SpeculationEagerness>> prerenders_;
109 PrerenderCancellationCallback prerender_cancellation_callback_ =
110 base::DoNothing();
Iman Saboori31bed872022-12-09 15:41:31111};
112
113class ScopedMockPrerenderer {
114 public:
115 explicit ScopedMockPrerenderer(PreloadingDecider* preloading_decider)
116 : preloading_decider_(preloading_decider) {
117 auto new_prerenderer = std::make_unique<MockPrerenderer>();
118 prerenderer_ = new_prerenderer.get();
119 old_prerenderer_ = preloading_decider_->SetPrerendererForTesting(
120 std::move(new_prerenderer));
121 }
122
123 ~ScopedMockPrerenderer() {
Jagadesh P2b3158712023-09-25 18:42:42124 prerenderer_ = nullptr;
Iman Saboori31bed872022-12-09 15:41:31125 preloading_decider_->SetPrerendererForTesting(std::move(old_prerenderer_));
126 }
127
128 MockPrerenderer* Get() { return prerenderer_.get(); }
129
130 private:
Jagadesh P85c3f692023-10-16 08:06:27131 raw_ptr<PreloadingDecider> preloading_decider_ = nullptr;
132 raw_ptr<MockPrerenderer> prerenderer_ = nullptr;
Iman Saboori31bed872022-12-09 15:41:31133 std::unique_ptr<Prerenderer> old_prerenderer_;
134};
135
Iman Saboori64dc4de2022-11-29 18:58:06136class MockContentBrowserClient : public TestContentBrowserClient {
137 public:
138 MockContentBrowserClient() {
139 old_browser_client_ = SetBrowserClientForTesting(this);
140 }
141 ~MockContentBrowserClient() override {
142 EXPECT_EQ(this, SetBrowserClientForTesting(old_browser_client_));
143 }
144
145 std::unique_ptr<AnchorElementPreconnectDelegate>
146 CreateAnchorElementPreconnectDelegate(
147 RenderFrameHost& render_frame_host) override {
148 auto delegate =
149 std::make_unique<MockAnchorElementPreconnector>(render_frame_host);
150 delegate_ = delegate.get();
151 return delegate;
152 }
153
154 MockAnchorElementPreconnector* GetDelegate() { return delegate_; }
155
156 private:
Jagadesh P85c3f692023-10-16 08:06:27157 raw_ptr<ContentBrowserClient> old_browser_client_ = nullptr;
158 raw_ptr<MockAnchorElementPreconnector> delegate_ = nullptr;
Iman Saboori64dc4de2022-11-29 18:58:06159};
160
Iman Saboorib38194772023-01-17 23:39:51161enum class EventType {
162 kPointerDown,
163 kPointerHover,
164};
165
Taiyo Mizuhashif3098ea2023-10-11 13:14:26166class PreloadingDeciderTest : public RenderViewHostTestHarness {
Iman Saboori64dc4de2022-11-29 18:58:06167 public:
Iman Saboori64dc4de2022-11-29 18:58:06168 void SetUp() override {
169 RenderViewHostTestHarness::SetUp();
170
Johannbecce082023-01-17 05:14:32171 web_contents_delegate_ =
172 std::make_unique<test::ScopedPrerenderWebContentsDelegate>(
Kevin McNee031e28c2024-03-21 20:55:00173 *web_contents());
174 NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
175 GetSameOriginUrl("/"));
Iman Saboori64dc4de2022-11-29 18:58:06176 prefetch_service_ =
177 std::make_unique<TestPrefetchService>(GetBrowserContext());
178 PrefetchDocumentManager::SetPrefetchServiceForTesting(
179 prefetch_service_.get());
180 }
181 void TearDown() override {
Jayson Adams402cc1c2023-11-27 19:46:49182 // The PrefetchService we created for the test contains a
183 // PrefetchOriginProber, which holds a raw pointer to the BrowserContext.
184 // When tearing down, it's important to free our PrefetchService
185 // before freeing the BrowserContext, to avoid any chance of a use after
186 // free.
187 PrefetchDocumentManager::SetPrefetchServiceForTesting(nullptr);
188 prefetch_service_.reset();
189
Iman Saboori64dc4de2022-11-29 18:58:06190 RenderViewHostTestHarness::TearDown();
191 }
192
193 RenderFrameHostImpl& GetPrimaryMainFrame() {
Kevin McNee031e28c2024-03-21 20:55:00194 return *static_cast<RenderFrameHostImpl*>(main_rfh());
Iman Saboori64dc4de2022-11-29 18:58:06195 }
196
197 GURL GetSameOriginUrl(const std::string& path) {
198 return GURL("https://example.com" + path);
199 }
200
201 GURL GetCrossOriginUrl(const std::string& path) {
202 return GURL("https://other.example.com" + path);
203 }
204
205 TestPrefetchService* GetPrefetchService() { return prefetch_service_.get(); }
206
Kevin McNee031e28c2024-03-21 20:55:00207 blink::mojom::SpeculationCandidatePtr MakeCandidate(
208 const GURL& url,
209 blink::mojom::SpeculationAction action,
210 blink::mojom::SpeculationEagerness eagerness) {
211 auto candidate = blink::mojom::SpeculationCandidate::New();
212 candidate->url = url;
213 candidate->action = action;
214 candidate->eagerness = eagerness;
215 candidate->referrer = blink::mojom::Referrer::New();
Hiroki Nakagawac91e36222025-05-02 11:00:27216 candidate->tags = {std::nullopt};
Kevin McNee031e28c2024-03-21 20:55:00217
218 return candidate;
219 }
220
Iman Saboori64dc4de2022-11-29 18:58:06221 private:
Iman Saboori31bed872022-12-09 15:41:31222 test::ScopedPrerenderFeatureList prerender_feature_list_;
Iman Saboori64dc4de2022-11-29 18:58:06223 std::unique_ptr<TestPrefetchService> prefetch_service_;
Johannbecce082023-01-17 05:14:32224 std::unique_ptr<test::ScopedPrerenderWebContentsDelegate>
225 web_contents_delegate_;
Iman Saboori64dc4de2022-11-29 18:58:06226};
227
Iman Saboori31bed872022-12-09 15:41:31228TEST_F(PreloadingDeciderTest, DefaultEagernessCandidatesStartOnStandby) {
229 auto* preloading_decider =
230 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
231 ASSERT_TRUE(preloading_decider != nullptr);
232
Takashi Nakayamae234c492025-08-04 03:16:48233 const bool use_eager_heurisctics =
234 base::FeatureList::IsEnabled(blink::features::kPreloadingEagerHeuristics);
235
Iman Saboori31bed872022-12-09 15:41:31236 // Create list of SpeculationCandidatePtrs.
237 std::vector<std::tuple<bool, GURL, blink::mojom::SpeculationAction,
238 blink::mojom::SpeculationEagerness>>
239 test_cases{{true, GetCrossOriginUrl("/candidate1.html"),
240 blink::mojom::SpeculationAction::kPrefetch,
Iman Saboori6e3bb0c2023-01-13 19:57:08241 blink::mojom::SpeculationEagerness::kConservative},
Iman Saboorib38194772023-01-17 23:39:51242 {true, GetCrossOriginUrl("/candidate2.html"),
243 blink::mojom::SpeculationAction::kPrefetch,
244 blink::mojom::SpeculationEagerness::kModerate},
Takashi Nakayamae234c492025-08-04 03:16:48245 {use_eager_heurisctics, GetCrossOriginUrl("/candidate3.html"),
246 blink::mojom::SpeculationAction::kPrefetch,
247 blink::mojom::SpeculationEagerness::kEager},
248 {false, GetCrossOriginUrl("/candidate4.html"),
Iman Saboori31bed872022-12-09 15:41:31249 blink::mojom::SpeculationAction::kPrefetch,
Takashi Nakayama978f0a152025-06-17 08:26:25250 blink::mojom::SpeculationEagerness::kImmediate},
Iman Saboori31bed872022-12-09 15:41:31251 {true, GetCrossOriginUrl("/candidate1.html"),
252 blink::mojom::SpeculationAction::kPrerender,
Iman Saboori6e3bb0c2023-01-13 19:57:08253 blink::mojom::SpeculationEagerness::kConservative},
Iman Saboorib38194772023-01-17 23:39:51254 {true, GetCrossOriginUrl("/candidate2.html"),
255 blink::mojom::SpeculationAction::kPrerender,
256 blink::mojom::SpeculationEagerness::kModerate},
Takashi Nakayamae234c492025-08-04 03:16:48257 {use_eager_heurisctics, GetCrossOriginUrl("/candidate3.html"),
258 blink::mojom::SpeculationAction::kPrerender,
259 blink::mojom::SpeculationEagerness::kEager},
260 {false, GetCrossOriginUrl("/candidate4.html"),
Iman Saboori31bed872022-12-09 15:41:31261 blink::mojom::SpeculationAction::kPrerender,
Takashi Nakayama978f0a152025-06-17 08:26:25262 blink::mojom::SpeculationEagerness::kImmediate}};
Iman Saboori31bed872022-12-09 15:41:31263 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
264 for (const auto& [should_be_on_standby, url, action, eagerness] :
265 test_cases) {
Kevin McNee031e28c2024-03-21 20:55:00266 candidates.push_back(MakeCandidate(url, action, eagerness));
Iman Saboori31bed872022-12-09 15:41:31267 }
268
Adithya Srinivasan94ec2fea2023-05-01 17:02:15269 preloading_decider->UpdateSpeculationCandidates(candidates);
Iman Saboori31bed872022-12-09 15:41:31270
271 for (const auto& [should_be_on_standby, url, action, eagerness] :
272 test_cases) {
273 EXPECT_EQ(should_be_on_standby,
274 preloading_decider->IsOnStandByForTesting(url, action));
275 }
276}
277
Taiyo Mizuhashif3098ea2023-10-11 13:14:26278class PreloadingDeciderPointerEventHeuristicsTest
279 : public PreloadingDeciderTest,
280 public ::testing::WithParamInterface<
Takashi Nakayamae234c492025-08-04 03:16:48281 std::tuple<EventType, blink::mojom::SpeculationEagerness>> {
282 public:
283 void SetUp() override {
284 feature_list_.InitAndEnableFeature(
285 blink::features::kPreloadingEagerHeuristics);
286 PreloadingDeciderTest::SetUp();
287 }
288
289 void TearDown() override {
290 PreloadingDeciderTest::TearDown();
291 feature_list_.Reset();
292 }
293
294 private:
295 base::test::ScopedFeatureList feature_list_;
296};
Taiyo Mizuhashif3098ea2023-10-11 13:14:26297
298TEST_P(PreloadingDeciderPointerEventHeuristicsTest,
299 PrefetchOnPointerEventHeuristics) {
Jeremy Roman2cf0e772023-03-13 23:20:00300 const auto [event_type, eagerness] = GetParam();
301
Iman Saboori64dc4de2022-11-29 18:58:06302 MockContentBrowserClient browser_client;
303
304 auto* preloading_decider =
305 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
Iman Saboori31bed872022-12-09 15:41:31306 ASSERT_TRUE(preloading_decider != nullptr);
Iman Saboori64dc4de2022-11-29 18:58:06307
308 auto* preconnect_delegate = browser_client.GetDelegate();
309 EXPECT_TRUE(preconnect_delegate != nullptr);
310
311 // Create list of SpeculationCandidatePtrs.
312 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
313
Jeremy Roman2cf0e772023-03-13 23:20:00314 auto call_pointer_event_handler = [&](const GURL& url) {
315 switch (event_type) {
Iman Saboorib38194772023-01-17 23:39:51316 case EventType::kPointerDown:
317 preloading_decider->OnPointerDown(url);
318 break;
319 case EventType::kPointerHover:
Iman Saboori50d51092023-06-09 22:17:30320 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:48321 url, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0),
322 eagerness);
Iman Saboorib38194772023-01-17 23:39:51323 break;
324 }
325 };
326
Kevin McNee031e28c2024-03-21 20:55:00327 auto candidate1 =
328 MakeCandidate(GetCrossOriginUrl("/candidate1.html"),
329 blink::mojom::SpeculationAction::kPrefetch, eagerness);
Iman Saboori64dc4de2022-11-29 18:58:06330 candidate1->requires_anonymous_client_ip_when_cross_origin = true;
Iman Saboori64dc4de2022-11-29 18:58:06331 candidates.push_back(std::move(candidate1));
332
Adithya Srinivasan94ec2fea2023-05-01 17:02:15333 preloading_decider->UpdateSpeculationCandidates(candidates);
Iman Saboorib38194772023-01-17 23:39:51334 // It should not pass kModerate or kConservative candidates directly
Iman Saboori64dc4de2022-11-29 18:58:06335 EXPECT_TRUE(GetPrefetchService()->prefetches_.empty());
336
Jeremy Roman2cf0e772023-03-13 23:20:00337 // By default, pointer hover is not enough to trigger conservative candidates.
338 if (std::pair(event_type, eagerness) !=
339 std::pair(EventType::kPointerHover,
340 blink::mojom::SpeculationEagerness::kConservative)) {
341 call_pointer_event_handler(GetCrossOriginUrl("/candidate1.html"));
342 EXPECT_FALSE(
343 preconnect_delegate->Target().has_value()); // Shouldn't preconnect
344 EXPECT_EQ(
345 1u,
346 GetPrefetchService()->prefetches_.size()); // It should only prefetch
Iman Saboori64dc4de2022-11-29 18:58:06347
Jeremy Roman2cf0e772023-03-13 23:20:00348 // Another pointer event should not change anything
349 call_pointer_event_handler(GetCrossOriginUrl("/candidate1.html"));
350 EXPECT_FALSE(preconnect_delegate->Target().has_value());
351 EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
Iman Saboori64dc4de2022-11-29 18:58:06352
Jeremy Roman2cf0e772023-03-13 23:20:00353 call_pointer_event_handler(GetCrossOriginUrl("/candidate2.html"));
Iman Saboori9fa1e242023-06-16 20:49:11354 // It should preconnect if the target is not safe to prefetch and it is a
355 // `kPointerDown` event.
356 switch (event_type) {
357 case EventType::kPointerDown:
358 EXPECT_TRUE(preconnect_delegate->Target().has_value());
359 break;
360 case EventType::kPointerHover:
361 EXPECT_FALSE(preconnect_delegate->Target().has_value());
362 break;
363 }
Jeremy Roman2cf0e772023-03-13 23:20:00364 EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
365 } else {
366 call_pointer_event_handler(GetCrossOriginUrl("/candidate1.html"));
Iman Saboori9fa1e242023-06-16 20:49:11367 // It should preconnect if the target is not safe to prefetch and it is a
368 // `kPointerDown` event.
369 switch (event_type) {
370 case EventType::kPointerDown:
371 EXPECT_TRUE(preconnect_delegate->Target().has_value());
372 break;
373 case EventType::kPointerHover:
374 EXPECT_FALSE(preconnect_delegate->Target().has_value());
375 break;
376 }
Jeremy Roman2cf0e772023-03-13 23:20:00377 EXPECT_EQ(0u, GetPrefetchService()->prefetches_.size());
378
379 call_pointer_event_handler(GetCrossOriginUrl("/candidate2.html"));
Iman Saboori9fa1e242023-06-16 20:49:11380 // It should preconnect if the target is not safe to prefetch and it is a
381 // `kPointerDown` event.
382 switch (event_type) {
383 case EventType::kPointerDown:
384 EXPECT_TRUE(preconnect_delegate->Target().has_value());
385 break;
386 case EventType::kPointerHover:
387 EXPECT_FALSE(preconnect_delegate->Target().has_value());
388 break;
389 }
Jeremy Roman2cf0e772023-03-13 23:20:00390 EXPECT_EQ(0u, GetPrefetchService()->prefetches_.size());
391 }
Iman Saboori64dc4de2022-11-29 18:58:06392}
393
Taiyo Mizuhashif3098ea2023-10-11 13:14:26394TEST_P(PreloadingDeciderPointerEventHeuristicsTest,
395 PrerenderOnPointerEventHeuristics) {
Jeremy Roman2cf0e772023-03-13 23:20:00396 const auto [event_type, eagerness] = GetParam();
397
Iman Saboori31bed872022-12-09 15:41:31398 MockContentBrowserClient browser_client;
399
400 auto* preloading_decider =
401 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
402 ASSERT_TRUE(preloading_decider != nullptr);
403
404 ScopedMockPrerenderer prerenderer(preloading_decider);
405
406 auto* preconnect_delegate = browser_client.GetDelegate();
407 EXPECT_TRUE(preconnect_delegate != nullptr);
408
409 // Create list of SpeculationCandidatePtrs.
410 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
411
Liviu Tinta8a3a7bc2023-05-23 00:44:34412 auto create_candidate =
413 [&](blink::mojom::SpeculationAction action, const std::string& url,
414 network::mojom::NoVarySearchPtr&& no_vary_search_hint = nullptr) {
Kevin McNee031e28c2024-03-21 20:55:00415 auto candidate =
416 MakeCandidate(GetSameOriginUrl(url), action, eagerness);
Liviu Tinta8a3a7bc2023-05-23 00:44:34417 if (no_vary_search_hint) {
418 candidate->no_vary_search_hint = std::move(no_vary_search_hint);
419 }
420 return candidate;
421 };
Iman Saboori31bed872022-12-09 15:41:31422
Jeremy Roman2cf0e772023-03-13 23:20:00423 auto call_pointer_event_handler = [&](const GURL& url) {
424 switch (event_type) {
Iman Saboorib38194772023-01-17 23:39:51425 case EventType::kPointerDown:
426 preloading_decider->OnPointerDown(url);
427 break;
428 case EventType::kPointerHover:
Iman Saboori50d51092023-06-09 22:17:30429 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:48430 url, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0),
431 eagerness);
Iman Saboorib38194772023-01-17 23:39:51432 break;
433 }
434 };
435
Jeremy Roman2cf0e772023-03-13 23:20:00436 candidates.push_back(create_candidate(
437 blink::mojom::SpeculationAction::kPrerender, "/candidate1.html"));
438 candidates.push_back(create_candidate(
439 blink::mojom::SpeculationAction::kPrefetch, "/candidate2.html"));
Liviu Tinta8a3a7bc2023-05-23 00:44:34440 candidates.push_back(create_candidate(
441 blink::mojom::SpeculationAction::kPrefetch, "/candidate4.html?a=1",
442 network::mojom::NoVarySearch::New(
443 network::mojom::SearchParamsVariance::NewNoVaryParams({"a"}), true)));
Liviu Tinta52efaa062024-06-14 22:56:42444 candidates.push_back(create_candidate(
445 blink::mojom::SpeculationAction::kPrerender, "/candidate5.html?a=1",
446 network::mojom::NoVarySearch::New(
447 network::mojom::SearchParamsVariance::NewNoVaryParams({"a"}), true)));
Iman Saboori31bed872022-12-09 15:41:31448
Adithya Srinivasan94ec2fea2023-05-01 17:02:15449 preloading_decider->UpdateSpeculationCandidates(candidates);
Iman Saboorib38194772023-01-17 23:39:51450 // It should not pass kModerate or kConservative candidates directly
Iman Saboori31bed872022-12-09 15:41:31451 EXPECT_TRUE(prerenderer.Get()->prerenders_.empty());
452 EXPECT_TRUE(GetPrefetchService()->prefetches_.empty());
453
Jeremy Roman2cf0e772023-03-13 23:20:00454 // By default, pointer hover is not enough to trigger conservative candidates.
455 if (std::pair(event_type, eagerness) !=
456 std::pair(EventType::kPointerHover,
457 blink::mojom::SpeculationEagerness::kConservative)) {
458 call_pointer_event_handler(GetSameOriginUrl("/candidate1.html"));
459 EXPECT_FALSE(
460 preconnect_delegate->Target().has_value()); // Shouldn't preconnect.
461 EXPECT_EQ(0u,
462 GetPrefetchService()->prefetches_.size()); // Shouldn't prefetch.
463 EXPECT_EQ(1u,
464 prerenderer.Get()->prerenders_.size()); // Should prerender.
Iman Saboori31bed872022-12-09 15:41:31465
Jeremy Roman2cf0e772023-03-13 23:20:00466 // Another pointer event should not change anything
467 call_pointer_event_handler(GetSameOriginUrl("/candidate1.html"));
Iman Saboorib38194772023-01-17 23:39:51468
Jeremy Roman2cf0e772023-03-13 23:20:00469 EXPECT_FALSE(preconnect_delegate->Target().has_value());
470 EXPECT_EQ(0u, GetPrefetchService()->prefetches_.size());
471 EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
Iman Saboori31bed872022-12-09 15:41:31472
Jeremy Roman2cf0e772023-03-13 23:20:00473 // It should prefetch if the target is safe to prefetch.
474 call_pointer_event_handler(GetSameOriginUrl("/candidate2.html"));
475 EXPECT_FALSE(preconnect_delegate->Target().has_value());
476 EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
477 EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
Iman Saboori31bed872022-12-09 15:41:31478
Liviu Tinta8a3a7bc2023-05-23 00:44:34479 // It should prefetch if there is a prefetch candidate matching by
480 // No-Vary-Search hint.
481 call_pointer_event_handler(GetSameOriginUrl("/candidate4.html"));
482 EXPECT_FALSE(preconnect_delegate->Target().has_value());
483 EXPECT_EQ(2u, GetPrefetchService()->prefetches_.size());
484 EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
485
Liviu Tinta52efaa062024-06-14 22:56:42486 // It should prerender if there is a prerender candidate matching by
487 // No-Vary-Search hint.
488 call_pointer_event_handler(GetSameOriginUrl("/candidate5.html"));
489 EXPECT_FALSE(
490 preconnect_delegate->Target().has_value()); // Shouldn't preconnect
491 EXPECT_EQ(2u,
492 GetPrefetchService()->prefetches_.size()); // Shouldn't prefetch
493 EXPECT_EQ(2u, prerenderer.Get()->prerenders_.size()); // Should prerender
494
Jeremy Roman2cf0e772023-03-13 23:20:00495 call_pointer_event_handler(GetSameOriginUrl("/candidate3.html"));
Iman Saboori9fa1e242023-06-16 20:49:11496 // It should preconnect if the target is not safe to prerender nor safe to
497 // prefetch and it is a `kPointerDown` event.
498 switch (event_type) {
499 case EventType::kPointerDown:
500 EXPECT_TRUE(preconnect_delegate->Target().has_value());
501 break;
502 case EventType::kPointerHover:
503 EXPECT_FALSE(preconnect_delegate->Target().has_value());
504 break;
505 }
Liviu Tinta8a3a7bc2023-05-23 00:44:34506 EXPECT_EQ(2u, GetPrefetchService()->prefetches_.size());
Liviu Tinta52efaa062024-06-14 22:56:42507 EXPECT_EQ(2u, prerenderer.Get()->prerenders_.size());
Jeremy Roman2cf0e772023-03-13 23:20:00508 } else {
509 call_pointer_event_handler(GetSameOriginUrl("/candidate1.html"));
Iman Saboori9fa1e242023-06-16 20:49:11510 // It should preconnect if the target is not safe to prerender nor safe to
511 // prefetch and it is a `kPointerDown` event.
512 switch (event_type) {
513 case EventType::kPointerDown:
514 EXPECT_TRUE(preconnect_delegate->Target().has_value());
515 break;
516 case EventType::kPointerHover:
517 EXPECT_FALSE(preconnect_delegate->Target().has_value());
518 break;
519 }
Jeremy Roman2cf0e772023-03-13 23:20:00520 EXPECT_EQ(0u, GetPrefetchService()->prefetches_.size());
521 EXPECT_EQ(0u, prerenderer.Get()->prerenders_.size());
522 }
Iman Saboori31bed872022-12-09 15:41:31523}
524
Iman Saboorib38194772023-01-17 23:39:51525INSTANTIATE_TEST_SUITE_P(
Kevin McNee031e28c2024-03-21 20:55:00526 ParameterizedTests,
Taiyo Mizuhashif3098ea2023-10-11 13:14:26527 PreloadingDeciderPointerEventHeuristicsTest,
Iman Saboorib38194772023-01-17 23:39:51528 testing::Combine(
529 testing::Values(EventType::kPointerDown, EventType::kPointerHover),
Takashi Nakayamae234c492025-08-04 03:16:48530 testing::Values(blink::mojom::SpeculationEagerness::kEager,
531 blink::mojom::SpeculationEagerness::kModerate,
Iman Saboorib38194772023-01-17 23:39:51532 blink::mojom::SpeculationEagerness::kConservative)));
533
Iman Saboorieda025c72023-05-12 18:55:29534TEST_F(PreloadingDeciderTest, UmaRecallStats) {
535 base::HistogramTester histogram_tester;
536 auto* preloading_decider =
537 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
538 ASSERT_TRUE(preloading_decider != nullptr);
539
540 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
Takashi Nakayama978f0a152025-06-17 08:26:25541 auto candidate =
542 MakeCandidate(GetCrossOriginUrl("/candidate1.html"),
543 blink::mojom::SpeculationAction::kPrefetch,
544 blink::mojom::SpeculationEagerness::kImmediate);
Iman Saboorieda025c72023-05-12 18:55:29545 candidates.push_back(std::move(candidate));
546
547 preloading_decider->UpdateSpeculationCandidates(candidates);
548
Minoru Chikamune68aacaf2025-02-20 09:48:01549 NavigationSimulator::NavigateAndCommitFromDocument(
550 GURL("https://www.google.com"), &GetPrimaryMainFrame());
Iman Saboorieda025c72023-05-12 18:55:29551
Minoru Chikamune68aacaf2025-02-20 09:48:01552 // Check recall.
553 const std::string kUmaName = base::StrCat(
554 {"Preloading.Predictor.",
555 preloading_predictor::kUrlPointerDownOnAnchor.name(), ".Recall"});
Iman Saboorieda025c72023-05-12 18:55:29556 histogram_tester.ExpectBucketCount(
Minoru Chikamune68aacaf2025-02-20 09:48:01557 kUmaName, PredictorConfusionMatrix::kTruePositive, 0);
Iman Saboorieda025c72023-05-12 18:55:29558 histogram_tester.ExpectBucketCount(
Minoru Chikamune68aacaf2025-02-20 09:48:01559 kUmaName, PredictorConfusionMatrix::kFalseNegative, 1);
Iman Saboorieda025c72023-05-12 18:55:29560}
561
HuanPo Lin9b730372025-03-28 05:50:45562// Test that speculation rules tags merging works as expected if multiple
563// matched rules applies.
Takashi Nakayama978f0a152025-06-17 08:26:25564TEST_F(PreloadingDeciderTest,
565 SpeculationRulesTagsMergingForNonImmediatePrefetch) {
HuanPo Lin9b730372025-03-28 05:50:45566 const GURL url = GetSameOriginUrl("/candidate1.html");
567 auto* preloading_decider =
568 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
569 ASSERT_TRUE(preloading_decider);
570
571 auto candidate_1 =
572 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
573 blink::mojom::SpeculationEagerness::kConservative);
574
575 auto candidate_2 = candidate_1.Clone();
576 candidate_2->eagerness = blink::mojom::SpeculationEagerness::kModerate;
577
578 candidate_1->tags = {"tag1"};
579 candidate_2->tags = {"tag2"};
580
581 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
582 candidates.push_back(candidate_1.Clone());
583 candidates.push_back(candidate_2.Clone());
584
585 // Add conservative and moderate preload candidate and preload on
586 // pointer-down.
587 preloading_decider->UpdateSpeculationCandidates(candidates);
588 const auto& prefetches = GetPrefetchService()->prefetches_;
589 preloading_decider->OnPointerDown(url);
590
Hiroshige Hayashizaki995741b2025-08-19 18:41:13591 EXPECT_TRUE(prefetches[0]->request().speculation_rules_tags());
592 EXPECT_EQ(prefetches[0]
593 ->request()
594 .speculation_rules_tags()
595 ->ConvertStringToHeaderString(),
HuanPo Lin9b730372025-03-28 05:50:45596 "\"tag1\", \"tag2\"");
597 EXPECT_FALSE(preloading_decider->IsOnStandByForTesting(
598 url, blink::mojom::SpeculationAction::kPrefetch));
599}
600
601// Test that no speculation rules tags merging happens if multiple candidates
602// are in the queue but only one is enacted.
603TEST_F(PreloadingDeciderTest,
Takashi Nakayama978f0a152025-06-17 08:26:25604 SpeculationRulesTagsNoMergingForNonImmediatePrefetch) {
HuanPo Lin9b730372025-03-28 05:50:45605 const GURL url = GetSameOriginUrl("/candidate1.html");
606 auto* preloading_decider =
607 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
608 ASSERT_TRUE(preloading_decider);
609
610 auto candidate_1 =
611 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
612 blink::mojom::SpeculationEagerness::kConservative);
613
614 auto candidate_2 = candidate_1.Clone();
615 candidate_2->eagerness = blink::mojom::SpeculationEagerness::kModerate;
616
617 candidate_1->tags = {"tag1"};
618 candidate_2->tags = {"tag2"};
619
620 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
621 candidates.push_back(candidate_1.Clone());
622 candidates.push_back(candidate_2.Clone());
623
Takashi Nakayamae234c492025-08-04 03:16:48624 // Add conservative and moderate preload candidate and preload on
HuanPo Lin9b730372025-03-28 05:50:45625 // pointer-hover.
626 preloading_decider->UpdateSpeculationCandidates(candidates);
627 const auto& prefetches = GetPrefetchService()->prefetches_;
628 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:48629 url,
630 blink::mojom::AnchorElementPointerData::New(
631 /*is_mouse_pointer=*/true,
632 /*mouse_velocity=*/75.0,
633 /*mouse_acceleration=*/0.0),
634 blink::mojom::SpeculationEagerness::kModerate);
HuanPo Lin9b730372025-03-28 05:50:45635
Hiroshige Hayashizaki995741b2025-08-19 18:41:13636 EXPECT_TRUE(prefetches[0]->request().speculation_rules_tags());
637 EXPECT_EQ(prefetches[0]
638 ->request()
639 .speculation_rules_tags()
640 ->ConvertStringToHeaderString(),
HuanPo Lin9b730372025-03-28 05:50:45641 "\"tag2\"");
642 EXPECT_FALSE(preloading_decider->IsOnStandByForTesting(
643 url, blink::mojom::SpeculationAction::kPrefetch));
644}
645
HuanPo Lin5860e8d2025-03-31 00:52:36646// Test that speculation rules tags merging works as expected if multiple
Takashi Nakayama978f0a152025-06-17 08:26:25647// immediate rules added.
648TEST_F(PreloadingDeciderTest, SpeculationRulesTagsMergingForImmediatePrefetch) {
HuanPo Lin5860e8d2025-03-31 00:52:36649 const GURL url = GetSameOriginUrl("/candidate1.html");
650 auto* preloading_decider =
651 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
652 ASSERT_TRUE(preloading_decider);
653
654 auto candidate_1 =
655 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
Takashi Nakayama978f0a152025-06-17 08:26:25656 blink::mojom::SpeculationEagerness::kImmediate);
HuanPo Lin5860e8d2025-03-31 00:52:36657
658 auto candidate_2 = candidate_1.Clone();
659
660 candidate_1->tags = {"tag1"};
661 candidate_2->tags = {"tag2"};
662
663 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
664 candidates.push_back(candidate_1.Clone());
665 candidates.push_back(candidate_2.Clone());
666
667 preloading_decider->UpdateSpeculationCandidates(candidates);
668 const auto& prefetches = GetPrefetchService()->prefetches_;
669
Hiroshige Hayashizaki995741b2025-08-19 18:41:13670 EXPECT_TRUE(prefetches[0]->request().speculation_rules_tags());
671 EXPECT_EQ(prefetches[0]
672 ->request()
673 .speculation_rules_tags()
674 ->ConvertStringToHeaderString(),
HuanPo Lin5860e8d2025-03-31 00:52:36675 "\"tag1\", \"tag2\"");
676 EXPECT_FALSE(preloading_decider->IsOnStandByForTesting(
677 url, blink::mojom::SpeculationAction::kPrefetch));
678}
679
Taiyo Mizuhashif3098ea2023-10-11 13:14:26680class PreloadingDeciderWithParameterizedSpeculationActionTest
681 : public PreloadingDeciderTest,
682 public ::testing::WithParamInterface<blink::mojom::SpeculationAction> {
683 public:
684 PreloadingDeciderWithParameterizedSpeculationActionTest() = default;
685
686 void SetUp() override {
687 PreloadingDeciderTest::SetUp();
688
689 if (GetSpeculationAction() == blink::mojom::SpeculationAction::kPrerender) {
690 old_prerenderer_ =
691 PreloadingDecider::GetOrCreateForCurrentDocument(
692 &GetPrimaryMainFrame())
693 ->SetPrerendererForTesting(std::make_unique<MockPrerenderer>());
694 }
695 }
696
697 void TearDown() override {
698 if (old_prerenderer_) {
699 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame())
700 ->SetPrerendererForTesting(std::move(old_prerenderer_));
701 }
702
703 PreloadingDeciderTest::TearDown();
704 }
705
706 blink::mojom::SpeculationAction GetSpeculationAction() { return GetParam(); }
707
708 MockPrerenderer* GetPrerenderer() {
709 return static_cast<MockPrerenderer*>(
710 &PreloadingDecider::GetOrCreateForCurrentDocument(
711 &GetPrimaryMainFrame())
712 ->GetPrerendererForTesting());
713 }
714
715 size_t GetNumOfExistingPreloads() {
716 switch (GetSpeculationAction()) {
717 case blink::mojom::SpeculationAction::kPrefetch:
718 return GetPrefetchService()->prefetches_.size();
719 case blink::mojom::SpeculationAction::kPrefetchWithSubresources:
Lingqi Chib19879f2025-07-16 05:48:13720 case blink::mojom::SpeculationAction::kPrerenderUntilScript:
Peter Boström01ab59a2024-08-15 02:39:49721 NOTREACHED();
Taiyo Mizuhashif3098ea2023-10-11 13:14:26722 case blink::mojom::SpeculationAction::kPrerender:
723 return GetPrerenderer()->prerenders_.size();
724 }
725 }
726
727 void DiscardPreloads(size_t index) {
728 switch (GetSpeculationAction()) {
729 case blink::mojom::SpeculationAction::kPrefetch:
730 GetPrefetchService()->EvictPrefetch(index);
731 break;
732 case blink::mojom::SpeculationAction::kPrefetchWithSubresources:
Lingqi Chib19879f2025-07-16 05:48:13733 case blink::mojom::SpeculationAction::kPrerenderUntilScript:
Peter Boström01ab59a2024-08-15 02:39:49734 NOTREACHED();
Taiyo Mizuhashif3098ea2023-10-11 13:14:26735 case blink::mojom::SpeculationAction::kPrerender:
736 GetPrerenderer()->OnCancel(index);
737 break;
738 }
739 }
740
741 private:
742 std::unique_ptr<Prerenderer> old_prerenderer_;
743};
744
745INSTANTIATE_TEST_SUITE_P(
Kevin McNee031e28c2024-03-21 20:55:00746 ParameterizedTests,
Taiyo Mizuhashif3098ea2023-10-11 13:14:26747 PreloadingDeciderWithParameterizedSpeculationActionTest,
748 testing::Values(blink::mojom::SpeculationAction::kPrefetch,
749 blink::mojom::SpeculationAction::kPrerender),
750 [](const testing::TestParamInfo<blink::mojom::SpeculationAction>& info) {
751 switch (info.param) {
752 case blink::mojom::SpeculationAction::kPrefetch:
753 return "kPrefetch";
754 case blink::mojom::SpeculationAction::kPrefetchWithSubresources:
Lingqi Chib19879f2025-07-16 05:48:13755 case blink::mojom::SpeculationAction::kPrerenderUntilScript:
Peter Boström01ab59a2024-08-15 02:39:49756 NOTREACHED();
Taiyo Mizuhashif3098ea2023-10-11 13:14:26757 case blink::mojom::SpeculationAction::kPrerender:
758 return "kPrerender";
759 }
760 });
761
Takashi Nakayama978f0a152025-06-17 08:26:25762// Tests that an immediate candidate is always processed before a non-immediate
763// candidate with the same URL, and that the non-immediate candidate isn't
764// marked as "on-standby".
Taiyo Mizuhashif3098ea2023-10-11 13:14:26765TEST_P(PreloadingDeciderWithParameterizedSpeculationActionTest,
766 CandidateWithMultipleEagernessValues) {
Adithya Srinivasand1c68ba2023-05-30 19:54:25767 const GURL url = GetSameOriginUrl("/candidate1.html");
768 auto* preloading_decider =
769 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
770 ASSERT_TRUE(preloading_decider);
771
Kevin McNee031e28c2024-03-21 20:55:00772 auto candidate_1 =
773 MakeCandidate(url, GetSpeculationAction(),
774 blink::mojom::SpeculationEagerness::kConservative);
Adithya Srinivasand1c68ba2023-05-30 19:54:25775
776 auto candidate_2 = candidate_1.Clone();
Takashi Nakayama978f0a152025-06-17 08:26:25777 candidate_2->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
Adithya Srinivasand1c68ba2023-05-30 19:54:25778
779 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
780 candidates.push_back(candidate_1.Clone());
781 candidates.push_back(candidate_2.Clone());
782
Taiyo Mizuhashif3098ea2023-10-11 13:14:26783 // Add conservative preload candidate and preload on pointer-down.
Adithya Srinivasand1c68ba2023-05-30 19:54:25784 preloading_decider->UpdateSpeculationCandidates(candidates);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26785 ASSERT_EQ(1u, GetNumOfExistingPreloads());
786 auto get_preload_eagerness = [&]() {
787 switch (GetSpeculationAction()) {
788 case blink::mojom::SpeculationAction::kPrefetch:
789 return GetPrefetchService()
790 ->prefetches_[0]
Hiroshige Hayashizakif55280f62025-08-19 17:23:20791 ->request()
792 .prefetch_type()
Taiyo Mizuhashif3098ea2023-10-11 13:14:26793 .GetEagerness();
794 case blink::mojom::SpeculationAction::kPrefetchWithSubresources:
Lingqi Chib19879f2025-07-16 05:48:13795 case blink::mojom::SpeculationAction::kPrerenderUntilScript:
Peter Boström01ab59a2024-08-15 02:39:49796 NOTREACHED();
Taiyo Mizuhashif3098ea2023-10-11 13:14:26797 case blink::mojom::SpeculationAction::kPrerender:
798 const auto& [_, eagerness] = GetPrerenderer()->prerenders_[0];
799 return eagerness;
800 }
801 };
802 EXPECT_EQ(get_preload_eagerness(),
Takashi Nakayama978f0a152025-06-17 08:26:25803 blink::mojom::SpeculationEagerness::kImmediate);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26804 EXPECT_FALSE(
805 preloading_decider->IsOnStandByForTesting(url, GetSpeculationAction()));
Adithya Srinivasand1c68ba2023-05-30 19:54:25806}
807
Taiyo Mizuhashif3098ea2023-10-11 13:14:26808// Tests that a previously preloaded conservative candidate can be
809// reprocessed after discard (when retriggered).
810TEST_P(PreloadingDeciderWithParameterizedSpeculationActionTest,
811 CandidateCanBeReprefetchedAfterDiscard) {
Adithya Srinivasand1c68ba2023-05-30 19:54:25812 const GURL url = GetSameOriginUrl("/candidate1.html");
813 auto* preloading_decider =
814 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
815 ASSERT_TRUE(preloading_decider);
816
Kevin McNee031e28c2024-03-21 20:55:00817 auto candidate =
818 MakeCandidate(url, GetSpeculationAction(),
819 blink::mojom::SpeculationEagerness::kConservative);
Adithya Srinivasand1c68ba2023-05-30 19:54:25820 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
821 candidates.push_back(candidate.Clone());
822
Taiyo Mizuhashif3098ea2023-10-11 13:14:26823 // Add conservative preloading candidate and preload on pointer-down.
Adithya Srinivasand1c68ba2023-05-30 19:54:25824 preloading_decider->UpdateSpeculationCandidates(candidates);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26825 EXPECT_EQ(0u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25826 preloading_decider->OnPointerDown(url);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26827 EXPECT_EQ(1u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25828
Takashi Nakayama978f0a152025-06-17 08:26:25829 // Simulate discard of non-immediate preload.
Taiyo Mizuhashif3098ea2023-10-11 13:14:26830 DiscardPreloads(0);
831 EXPECT_EQ(0u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25832
Taiyo Mizuhashif3098ea2023-10-11 13:14:26833 // Trigger preload for same URL again, it should succeed.
Adithya Srinivasand1c68ba2023-05-30 19:54:25834 preloading_decider->OnPointerDown(url);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26835 EXPECT_EQ(1u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25836
Takashi Nakayama978f0a152025-06-17 08:26:25837 // Simulate discard of non-immediate preload.
Taiyo Mizuhashif3098ea2023-10-11 13:14:26838 DiscardPreloads(0);
839 EXPECT_EQ(0u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25840
Takashi Nakayama978f0a152025-06-17 08:26:25841 auto immediate_candidate = candidate.Clone();
842 candidate->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
Adithya Srinivasand1c68ba2023-05-30 19:54:25843 candidates.clear();
844 candidates.push_back(candidate.Clone());
Takashi Nakayama978f0a152025-06-17 08:26:25845 candidates.push_back(immediate_candidate.Clone());
Adithya Srinivasand1c68ba2023-05-30 19:54:25846
Takashi Nakayama978f0a152025-06-17 08:26:25847 // Add a new immediate candidate (but also send the old non-immediate
848 // candidate). A preload should automatically trigger.
Adithya Srinivasand1c68ba2023-05-30 19:54:25849 preloading_decider->UpdateSpeculationCandidates(candidates);
Taiyo Mizuhashif3098ea2023-10-11 13:14:26850 EXPECT_EQ(1u, GetNumOfExistingPreloads());
Adithya Srinivasand1c68ba2023-05-30 19:54:25851}
852
Adithya Srinivasanb60483492023-06-14 21:22:14853// Tests that candidate removal causes a prefetch to be destroyed, and that
854// a reinserted candidate with the same url is re-processed.
Takashi Nakayama978f0a152025-06-17 08:26:25855TEST_F(PreloadingDeciderTest, ProcessCandidates_ImmediateCandidateRemoval) {
Adithya Srinivasanb60483492023-06-14 21:22:14856 auto* preloading_decider =
857 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
858 ASSERT_TRUE(preloading_decider);
859 const GURL url_1 = GetSameOriginUrl("/candidate1.html");
860 const GURL url_2 = GetSameOriginUrl("/candidate2.html");
861
Kevin McNee031e28c2024-03-21 20:55:00862 auto candidate_1 =
863 MakeCandidate(url_1, blink::mojom::SpeculationAction::kPrefetch,
Takashi Nakayama978f0a152025-06-17 08:26:25864 blink::mojom::SpeculationEagerness::kImmediate);
Adithya Srinivasanb60483492023-06-14 21:22:14865 candidate_1->requires_anonymous_client_ip_when_cross_origin = false;
866
867 auto candidate_2 = candidate_1.Clone();
868 candidate_2->url = url_2;
869
870 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
871 candidates.push_back(candidate_1.Clone());
872 candidates.push_back(candidate_2.Clone());
873 preloading_decider->UpdateSpeculationCandidates(candidates);
874
875 const auto& prefetches = GetPrefetchService()->prefetches_;
876 ASSERT_EQ(2u, prefetches.size());
877 EXPECT_EQ(prefetches[0]->GetURL(), url_1);
878 EXPECT_EQ(prefetches[1]->GetURL(), url_2);
879
880 // Remove |candidate_2|.
881 candidates.clear();
882 candidates.push_back(candidate_1.Clone());
883 preloading_decider->UpdateSpeculationCandidates(candidates);
884
885 EXPECT_TRUE(prefetches[0]);
886 EXPECT_FALSE(prefetches[1]);
887
888 // Re-add |candidate_2|.
889 candidates.clear();
890 candidates.push_back(candidate_1.Clone());
891 candidates.push_back(candidate_2.Clone());
892 preloading_decider->UpdateSpeculationCandidates(candidates);
893
894 ASSERT_EQ(3u, prefetches.size());
895 EXPECT_TRUE(prefetches[0]);
896 EXPECT_FALSE(prefetches[1]);
897 EXPECT_EQ(prefetches[2]->GetURL(), url_2);
898}
899
Takashi Nakayama978f0a152025-06-17 08:26:25900// Tests that candidate removal works correctly for non-immediate candidates,
901// and that a non-immediate candidate is reprocessed correctly after
902// re-insertion.
903TEST_F(PreloadingDeciderTest, ProcessCandidates_NonImmediateCandidateRemoval) {
Adithya Srinivasanb60483492023-06-14 21:22:14904 auto* preloading_decider =
905 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
906 ASSERT_TRUE(preloading_decider);
907 const GURL url_1 = GetSameOriginUrl("/candidate1.html");
908 const GURL url_2 = GetSameOriginUrl("/candidate2.html");
909
Kevin McNee031e28c2024-03-21 20:55:00910 auto candidate_1 =
911 MakeCandidate(url_1, blink::mojom::SpeculationAction::kPrefetch,
Takashi Nakayama978f0a152025-06-17 08:26:25912 blink::mojom::SpeculationEagerness::kImmediate);
Adithya Srinivasanb60483492023-06-14 21:22:14913
914 auto candidate_2 = candidate_1.Clone();
915 candidate_2->url = url_2;
916 candidate_2->eagerness = blink::mojom::SpeculationEagerness::kConservative;
917
918 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
919 candidates.push_back(candidate_1.Clone());
920 candidates.push_back(candidate_2.Clone());
921 preloading_decider->UpdateSpeculationCandidates(candidates);
922
923 const auto& prefetches = GetPrefetchService()->prefetches_;
924 ASSERT_EQ(1u, prefetches.size());
925 EXPECT_EQ(prefetches[0]->GetURL(), url_1);
926
927 preloading_decider->OnPointerDown(url_2);
928
929 ASSERT_EQ(2u, prefetches.size());
930 EXPECT_TRUE(prefetches[0]);
931 EXPECT_EQ(prefetches[1]->GetURL(), url_2);
932
933 // Remove |candidate_2|.
934 candidates.clear();
935 candidates.push_back(candidate_1.Clone());
936 preloading_decider->UpdateSpeculationCandidates(candidates);
937
938 ASSERT_EQ(2u, prefetches.size());
939 EXPECT_TRUE(prefetches[0]);
940 EXPECT_FALSE(prefetches[1]);
941
942 // Re-add |candidate_2|, remove |candidate_1|.
943 candidates.clear();
944 candidates.push_back(candidate_2.Clone());
945 preloading_decider->UpdateSpeculationCandidates(candidates);
946
947 ASSERT_EQ(2u, prefetches.size());
948 EXPECT_FALSE(prefetches[0]);
949
950 preloading_decider->OnPointerDown(url_2);
951
952 ASSERT_EQ(3u, prefetches.size());
953 EXPECT_TRUE(prefetches[2]);
954 EXPECT_EQ(prefetches[2]->GetURL(), url_2);
955}
956
957// Test to demonstrate current behaviour where a prefetch is still considered
958// to have a speculation candidate even if its original triggering speculation
959// candidate was removed; so long as there exists a candidate with the same
960// URL.
961TEST_F(PreloadingDeciderTest,
962 ProcessCandidates_SecondCandidateWithSameUrlKeepsPrefetchAlive) {
Adithya Srinivasanb60483492023-06-14 21:22:14963 auto* preloading_decider =
964 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
965 ASSERT_TRUE(preloading_decider);
966 const GURL url = GetSameOriginUrl("/candidate.html");
967
Kevin McNee031e28c2024-03-21 20:55:00968 auto candidate_1 =
969 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
Takashi Nakayama978f0a152025-06-17 08:26:25970 blink::mojom::SpeculationEagerness::kImmediate);
Adithya Srinivasanb60483492023-06-14 21:22:14971
972 auto candidate_2 = candidate_1.Clone();
973 candidate_2->eagerness = blink::mojom::SpeculationEagerness::kConservative;
974
975 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
976 candidates.push_back(candidate_1.Clone());
977 candidates.push_back(candidate_2.Clone());
978 preloading_decider->UpdateSpeculationCandidates(candidates);
979
980 const auto& prefetches = GetPrefetchService()->prefetches_;
981 ASSERT_EQ(prefetches.size(), 1u);
982 EXPECT_EQ(prefetches[0]->GetURL(), url);
983
984 // Remove |candidate_1|.
985 candidates.clear();
986 candidates.push_back(candidate_2.Clone());
987 preloading_decider->UpdateSpeculationCandidates(candidates);
988
989 EXPECT_EQ(prefetches.size(), 1u);
990 EXPECT_TRUE(prefetches[0]);
991}
992
Iman Saboori31f09492023-06-26 20:15:20993TEST_F(PreloadingDeciderTest,
994 OnPointerHoverWithMotionEstimatorIsRecordedToUMA) {
995 base::HistogramTester histogram_tester;
996 auto* preloading_decider =
997 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
998 ASSERT_TRUE(preloading_decider != nullptr);
999
1000 GURL url1{"https://www.example.com"};
1001 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:481002 url1,
1003 blink::mojom::AnchorElementPointerData::New(
1004 /*is_mouse_pointer=*/true,
1005 /*mouse_velocity=*/50.0,
1006 /*mouse_acceleration=*/0.0),
1007 /*is_eager=*/blink::mojom::SpeculationEagerness::kModerate);
Iman Saboori31f09492023-06-26 20:15:201008
1009 GURL url2{"https://www.google.com"};
1010 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:481011 url2,
1012 blink::mojom::AnchorElementPointerData::New(
1013 /*is_mouse_pointer=*/true,
1014 /*mouse_velocity=*/75.0,
1015 /*mouse_acceleration=*/0.0),
1016 /*is_eager=*/blink::mojom::SpeculationEagerness::kModerate);
Iman Saboori31f09492023-06-26 20:15:201017
1018 // Navigate to `url2`.
Kevin McNee031e28c2024-03-21 20:55:001019 NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), url2);
Iman Saboori31f09492023-06-26 20:15:201020
1021 // Check UMA records.
1022 histogram_tester.ExpectBucketCount(
1023 "Preloading.Experimental.OnPointerHoverWithMotionEstimator.Negative",
1024 /*100*(50-0/500)=*/10, 1);
1025 histogram_tester.ExpectBucketCount(
1026 "Preloading.Experimental.OnPointerHoverWithMotionEstimator.Negative",
1027 /*100*(75-0/500)=*/15, 0);
1028 histogram_tester.ExpectBucketCount(
1029 "Preloading.Experimental.OnPointerHoverWithMotionEstimator.Positive",
1030 /*100*(50-0/500)=*/10, 0);
1031 histogram_tester.ExpectBucketCount(
1032 "Preloading.Experimental.OnPointerHoverWithMotionEstimator.Positive",
1033 /*100*(75-0/500)=*/15, 1);
1034}
1035
Adithya Srinivasan7239d4f2024-11-05 15:35:531036TEST_F(PreloadingDeciderTest, ViewportHeuristicPredictionIsNotEnacted) {
1037 base::test::ScopedFeatureList feature_list;
Hiroki Nakagawa5c0b1802025-08-22 08:23:431038 feature_list.InitAndEnableFeatureWithParameters(
1039 blink::features::kPreloadingViewportHeuristics,
1040 {{"enact_candidates", "false"}});
Adithya Srinivasan7239d4f2024-11-05 15:35:531041
1042 auto* preloading_decider =
1043 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1044 ASSERT_TRUE(preloading_decider != nullptr);
1045
1046 const GURL url("https://example.com");
1047 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1048 auto candidate =
1049 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1050 blink::mojom::SpeculationEagerness::kModerate);
1051 candidates.push_back(std::move(candidate));
1052 preloading_decider->UpdateSpeculationCandidates(candidates);
1053
1054 preloading_decider->OnViewportHeuristicTriggered(url);
1055 const auto& prefetches = GetPrefetchService()->prefetches_;
1056 EXPECT_TRUE(prefetches.empty());
1057}
1058
1059TEST_F(PreloadingDeciderTest,
1060 ViewportHeuristicPredictionIsEnactedForModeratePrefetchCandidate) {
1061 base::test::ScopedFeatureList feature_list;
1062 feature_list.InitAndEnableFeatureWithParameters(
1063 blink::features::kPreloadingViewportHeuristics,
1064 {{"enact_candidates", "true"}});
1065
1066 base::HistogramTester histogram_tester;
1067 auto* preloading_decider =
1068 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1069 ASSERT_TRUE(preloading_decider != nullptr);
1070
1071 const GURL url("https://example.com");
1072 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1073 auto candidate =
1074 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1075 blink::mojom::SpeculationEagerness::kModerate);
1076 candidates.push_back(std::move(candidate));
1077 preloading_decider->UpdateSpeculationCandidates(candidates);
1078
1079 preloading_decider->OnViewportHeuristicTriggered(url);
1080 const auto& prefetches = GetPrefetchService()->prefetches_;
1081 ASSERT_EQ(prefetches.size(), 1u);
1082 EXPECT_EQ(prefetches[0]->GetURL(), url);
1083
1084 std::unique_ptr<NavigationSimulator> navigation =
1085 NavigationSimulator::CreateRendererInitiated(url, main_rfh());
1086 navigation->SetTransition(ui::PAGE_TRANSITION_LINK);
1087 navigation->Start();
1088
1089 histogram_tester.ExpectUniqueSample(
1090 "Preloading.Predictor.ViewportHeuristic.Precision",
1091 PredictorConfusionMatrix::kTruePositive, 1);
1092 histogram_tester.ExpectUniqueSample(
1093 "Preloading.Predictor.ViewportHeuristic.Recall",
1094 PredictorConfusionMatrix::kTruePositive, 1);
1095}
1096
1097TEST_F(PreloadingDeciderTest,
1098 ViewportHeuristicIsEnactedForModeratePrerenderCandidate) {
1099 base::test::ScopedFeatureList feature_list;
1100 feature_list.InitAndEnableFeatureWithParameters(
1101 blink::features::kPreloadingViewportHeuristics,
1102 {{"enact_candidates", "true"}});
1103
1104 auto* preloading_decider =
1105 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1106 ASSERT_TRUE(preloading_decider != nullptr);
1107 ScopedMockPrerenderer mock_prerender(preloading_decider);
1108
1109 const GURL url("https://example.com");
1110 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1111 auto candidate =
1112 MakeCandidate(url, blink::mojom::SpeculationAction::kPrerender,
1113 blink::mojom::SpeculationEagerness::kModerate);
1114 candidates.push_back(std::move(candidate));
1115 preloading_decider->UpdateSpeculationCandidates(candidates);
1116
1117 preloading_decider->OnViewportHeuristicTriggered(url);
1118 ASSERT_EQ(mock_prerender.Get()->prerenders_.size(), 1u);
1119 EXPECT_EQ(mock_prerender.Get()->prerenders_[0].first, url);
1120}
1121
1122TEST_F(PreloadingDeciderTest,
1123 ViewportHeuristicIsNotEnactedForConservativePrefetchCandidate) {
1124 base::test::ScopedFeatureList feature_list;
1125 feature_list.InitAndEnableFeatureWithParameters(
1126 blink::features::kPreloadingViewportHeuristics,
1127 {{"enact_candidates", "true"}});
1128
1129 auto* preloading_decider =
1130 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1131 ASSERT_TRUE(preloading_decider != nullptr);
1132
1133 const GURL url("https://example.com");
1134 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1135 auto candidate =
1136 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1137 blink::mojom::SpeculationEagerness::kConservative);
1138 candidates.push_back(std::move(candidate));
1139 preloading_decider->UpdateSpeculationCandidates(candidates);
1140
1141 preloading_decider->OnViewportHeuristicTriggered(url);
1142 const auto& prefetches = GetPrefetchService()->prefetches_;
1143 EXPECT_TRUE(prefetches.empty());
1144}
1145
Kevin McNee842eb0a2024-04-11 20:14:161146class PreloadingDeciderMLModelTest
1147 : public PreloadingDeciderTest,
1148 public ::testing::WithParamInterface<bool> {
Kevin McNee031e28c2024-03-21 20:55:001149 public:
1150 PreloadingDeciderMLModelTest() {
Kevin McNee842eb0a2024-04-11 20:14:161151 feature_list_.InitWithFeaturesAndParameters(
Adithya Srinivasanecb97f22024-09-25 19:02:261152 {{blink::features::kPreloadingHeuristicsMLModel,
Devon Loehrc0138d8d2025-03-04 01:11:421153 {{"enact_candidates", base::ToString(GetParam())}}}},
Kevin McNee842eb0a2024-04-11 20:14:161154 {});
Kevin McNee031e28c2024-03-21 20:55:001155 }
1156
1157 private:
1158 base::test::ScopedFeatureList feature_list_;
1159};
1160
Kevin McNee842eb0a2024-04-11 20:14:161161INSTANTIATE_TEST_SUITE_P(ParameterizedTests,
1162 PreloadingDeciderMLModelTest,
1163 testing::Bool());
1164
1165TEST_P(PreloadingDeciderMLModelTest, OnPreloadingHeuristicsModelDone) {
Iman Saboori86e1b6d2023-08-28 18:42:351166 base::HistogramTester histogram_tester;
1167
1168 GURL url1{"https://www.example.com"};
1169 GetPrimaryMainFrame().OnPreloadingHeuristicsModelDone(
1170 /*url=*/url1, /*score=*/0.2);
1171
1172 GURL url2{"https://www.google.com"};
1173 GetPrimaryMainFrame().OnPreloadingHeuristicsModelDone(
1174 /*url=*/url2, /*score=*/0.9);
1175
1176 // Navigate to `url2`.
Kevin McNee031e28c2024-03-21 20:55:001177 NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), url2);
Iman Saboori86e1b6d2023-08-28 18:42:351178
1179 // Check UMA records.
1180 histogram_tester.ExpectBucketCount(
1181 "Preloading.Experimental.OnPreloadingHeuristicsMLModel.Negative",
1182 /*100*0.2=*/20, 1);
1183 histogram_tester.ExpectBucketCount(
1184 "Preloading.Experimental.OnPreloadingHeuristicsMLModel.Negative",
1185 /*100*0.9=*/90, 0);
1186 histogram_tester.ExpectBucketCount(
1187 "Preloading.Experimental.OnPreloadingHeuristicsMLModel.Positive",
1188 /*100*0.2=*/20, 0);
1189 histogram_tester.ExpectBucketCount(
1190 "Preloading.Experimental.OnPreloadingHeuristicsMLModel.Positive",
1191 /*100*0.9=*/90, 1);
1192}
1193
Kevin McNee842eb0a2024-04-11 20:14:161194TEST_P(PreloadingDeciderMLModelTest, UseHoverHeuristicWhenNoMLModelPresent) {
1195 const GURL url = GetSameOriginUrl("/candidate1.html");
1196 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1197 candidates.push_back(
1198 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1199 blink::mojom::SpeculationEagerness::kModerate));
1200
1201 auto* preloading_decider =
1202 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1203 ASSERT_TRUE(preloading_decider);
1204 preloading_decider->UpdateSpeculationCandidates(candidates);
1205
1206 const auto& prefetches = GetPrefetchService()->prefetches_;
1207
1208 EXPECT_TRUE(prefetches.empty());
1209 // The page has never received a prediction from the ML model, so we fallback
1210 // to the decisions of the hover heuristic.
1211 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:481212 url, blink::mojom::AnchorElementPointerData::New(true, 0.0, 0.0),
1213 blink::mojom::SpeculationEagerness::kModerate);
Kevin McNee842eb0a2024-04-11 20:14:161214 EXPECT_EQ(1u, prefetches.size());
1215}
1216
1217class PreloadingDeciderMLModelActiveTest : public PreloadingDeciderTest {
1218 public:
1219 PreloadingDeciderMLModelActiveTest() {
1220 feature_list_.InitWithFeaturesAndParameters(
Adithya Srinivasanecb97f22024-09-25 19:02:261221 {{blink::features::kPreloadingHeuristicsMLModel,
1222 {{"enact_candidates", "true"},
1223 {"prefetch_moderate_threshold", "40"},
1224 {"prerender_moderate_threshold", "60"}}}},
Kevin McNee842eb0a2024-04-11 20:14:161225 {});
1226 }
1227
1228 private:
1229 base::test::ScopedFeatureList feature_list_;
1230};
1231
1232TEST_F(PreloadingDeciderMLModelActiveTest,
1233 ModelEnactsModeratePrefetchCandidate) {
1234 const GURL url = GetSameOriginUrl("/candidate1.html");
1235 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1236 candidates.push_back(
1237 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1238 blink::mojom::SpeculationEagerness::kModerate));
1239
1240 auto* preloading_decider =
1241 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1242 ASSERT_TRUE(preloading_decider);
1243 preloading_decider->UpdateSpeculationCandidates(candidates);
1244
1245 const auto& prefetches = GetPrefetchService()->prefetches_;
1246
1247 EXPECT_TRUE(prefetches.empty());
1248 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.99);
1249 EXPECT_EQ(1u, prefetches.size());
1250}
1251
1252TEST_F(PreloadingDeciderMLModelActiveTest,
1253 ModelEnactsModeratePrerenderCandidate) {
1254 const GURL url = GetSameOriginUrl("/candidate1.html");
1255 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1256 candidates.push_back(
1257 MakeCandidate(url, blink::mojom::SpeculationAction::kPrerender,
1258 blink::mojom::SpeculationEagerness::kModerate));
1259
1260 auto* preloading_decider =
1261 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1262 ASSERT_TRUE(preloading_decider);
1263 ScopedMockPrerenderer prerenderer(preloading_decider);
1264 preloading_decider->UpdateSpeculationCandidates(candidates);
1265
1266 const auto& prerenders = prerenderer.Get()->prerenders_;
1267
1268 EXPECT_TRUE(prerenders.empty());
1269 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.99);
1270 EXPECT_EQ(1u, prerenders.size());
1271}
1272
1273TEST_F(PreloadingDeciderMLModelActiveTest,
1274 ModelPrerendersCandidateOverPrefetch) {
1275 const GURL url = GetSameOriginUrl("/candidate1.html");
1276 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1277 candidates.push_back(
1278 MakeCandidate(url, blink::mojom::SpeculationAction::kPrerender,
1279 blink::mojom::SpeculationEagerness::kModerate));
1280 candidates.push_back(
1281 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1282 blink::mojom::SpeculationEagerness::kModerate));
1283
1284 auto* preloading_decider =
1285 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1286 ASSERT_TRUE(preloading_decider);
1287 ScopedMockPrerenderer prerenderer(preloading_decider);
1288 preloading_decider->UpdateSpeculationCandidates(candidates);
1289
1290 const auto& prefetches = GetPrefetchService()->prefetches_;
1291 const auto& prerenders = prerenderer.Get()->prerenders_;
1292
1293 EXPECT_TRUE(prefetches.empty());
1294 EXPECT_TRUE(prerenders.empty());
1295 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.99);
1296 EXPECT_TRUE(prefetches.empty());
1297 EXPECT_EQ(1u, prerenders.size());
1298}
1299
1300TEST_F(PreloadingDeciderMLModelActiveTest, ModelConfidenceThreshold) {
1301 const GURL url = GetSameOriginUrl("/candidate1.html");
1302 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1303 candidates.push_back(
1304 MakeCandidate(url, blink::mojom::SpeculationAction::kPrerender,
1305 blink::mojom::SpeculationEagerness::kModerate));
1306 candidates.push_back(
1307 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1308 blink::mojom::SpeculationEagerness::kModerate));
1309
1310 auto* preloading_decider =
1311 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1312 ASSERT_TRUE(preloading_decider);
1313 ScopedMockPrerenderer prerenderer(preloading_decider);
1314 preloading_decider->UpdateSpeculationCandidates(candidates);
1315
1316 const auto& prefetches = GetPrefetchService()->prefetches_;
1317 const auto& prerenders = prerenderer.Get()->prerenders_;
1318
1319 EXPECT_TRUE(prefetches.empty());
1320 EXPECT_TRUE(prerenders.empty());
1321 // The test is configured such that this is a high enough confidence for
1322 // prefetch, but not for prerender.
1323 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.50);
1324 EXPECT_EQ(1u, prefetches.size());
1325 EXPECT_TRUE(prerenders.empty());
1326}
1327
1328TEST_F(PreloadingDeciderMLModelActiveTest, ModelNoPreconnectFallback) {
1329 const GURL url = GetSameOriginUrl("/candidate1.html");
1330
1331 MockContentBrowserClient browser_client;
1332
1333 auto* preloading_decider =
1334 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1335 ASSERT_TRUE(preloading_decider);
1336 ScopedMockPrerenderer prerenderer(preloading_decider);
1337 auto* preconnect_delegate = browser_client.GetDelegate();
1338
1339 const auto& prefetches = GetPrefetchService()->prefetches_;
1340 const auto& prerenders = prerenderer.Get()->prerenders_;
1341
1342 EXPECT_FALSE(preconnect_delegate->Target().has_value());
1343 EXPECT_TRUE(prefetches.empty());
1344 EXPECT_TRUE(prerenders.empty());
1345 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.99);
1346 EXPECT_FALSE(preconnect_delegate->Target().has_value());
1347 EXPECT_TRUE(prefetches.empty());
1348 EXPECT_TRUE(prerenders.empty());
1349}
1350
1351TEST_F(PreloadingDeciderMLModelActiveTest, ModelSupersedesHoverHeuristic) {
1352 const GURL url = GetSameOriginUrl("/candidate1.html");
1353 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1354 candidates.push_back(
1355 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1356 blink::mojom::SpeculationEagerness::kModerate));
1357
1358 auto* preloading_decider =
1359 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1360 ASSERT_TRUE(preloading_decider);
1361 preloading_decider->UpdateSpeculationCandidates(candidates);
1362
1363 const auto& prefetches = GetPrefetchService()->prefetches_;
1364
1365 EXPECT_TRUE(prefetches.empty());
1366 preloading_decider->OnPreloadingHeuristicsModelDone(url, /*score=*/0.05);
1367 EXPECT_TRUE(prefetches.empty());
1368 // The model has indicated that the candidate is not worth prefetching, so we
1369 // should not prefetch based on the hover heuristic either.
1370 preloading_decider->OnPointerHover(
Takashi Nakayamae234c492025-08-04 03:16:481371 url, blink::mojom::AnchorElementPointerData::New(true, 0.0, 0.0),
1372 blink::mojom::SpeculationEagerness::kModerate);
Kevin McNee842eb0a2024-04-11 20:14:161373 EXPECT_TRUE(prefetches.empty());
1374 // But once we have a stronger signal like pointer down, we should prefetch.
1375 preloading_decider->OnPointerDown(url);
1376 EXPECT_EQ(1u, prefetches.size());
1377}
1378
Iman Saboori64dc4de2022-11-29 18:58:061379} // namespace
Rulong Chen(陈汝龙)88e06882025-07-14 12:57:241380
1381TEST_F(PreloadingDeciderTest, SpeculationRulesTagsMergingForNVSMatch) {
1382 auto* preloading_decider =
1383 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1384 ASSERT_TRUE(preloading_decider);
1385
1386 const GURL kPreloadingUrl1("https://example.com/page.html?id=1");
1387 const GURL kPreloadingUrl2("https://example.com/page.html?id=2");
1388 const GURL kActivationUrl("https://example.com/page.html?id=3");
1389
1390 auto make_nvs_candidate = [&](const GURL& url, const std::string& tag) {
1391 auto candidate =
1392 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1393 blink::mojom::SpeculationEagerness::kConservative);
1394 candidate->no_vary_search_hint = network::mojom::NoVarySearch::New(
1395 network::mojom::SearchParamsVariance::NewNoVaryParams({"id"}),
1396 /*vary_on_key_order=*/true);
1397 candidate->tags = {tag};
1398 return candidate;
1399 };
1400
1401 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1402 candidates.push_back(make_nvs_candidate(kPreloadingUrl1, "tag1"));
1403 candidates.push_back(make_nvs_candidate(kPreloadingUrl2, "tag2"));
1404
1405 preloading_decider->UpdateSpeculationCandidates(candidates);
1406
1407 // Both candidates should be on standby.
1408 EXPECT_TRUE(preloading_decider->IsOnStandByForTesting(
1409 kPreloadingUrl1, blink::mojom::SpeculationAction::kPrefetch));
1410 EXPECT_TRUE(preloading_decider->IsOnStandByForTesting(
1411 kPreloadingUrl2, blink::mojom::SpeculationAction::kPrefetch));
1412
1413 // Now, check if tags are merged correctly for a matching URL.
1414 const PreloadingDecider::SpeculationCandidateKey lookup_key{
1415 kActivationUrl, blink::mojom::SpeculationAction::kPrefetch};
1416 const PreloadingPredictor predictor =
1417 preloading_predictor::kUrlPointerDownOnAnchor;
1418
1419 auto merged_tags =
1420 preloading_decider->GetMergedSpeculationTagsFromSuitableCandidates(
Takashi Nakayama80498ee2025-07-31 05:31:311421 lookup_key, predictor, PreloadingConfidence{100},
1422 /*eagerness_to_exclude=*/{});
Rulong Chen(陈汝龙)88e06882025-07-14 12:57:241423
1424 // The merged tags should contain tags from both NVS-matched candidates.
1425 EXPECT_EQ(merged_tags.size(), 2u);
1426 EXPECT_TRUE(base::Contains(merged_tags, "tag1"));
1427 EXPECT_TRUE(base::Contains(merged_tags, "tag2"));
1428}
1429
1430TEST_F(PreloadingDeciderTest,
1431 SpeculationRulesTagsMergingForNVSMatchWithNullTags) {
1432 auto* preloading_decider =
1433 PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
1434 ASSERT_TRUE(preloading_decider);
1435
1436 const GURL kPreloadingUrl1("https://example.com/page.html?id=1");
1437 const GURL kPreloadingUrl2("https://example.com/page.html?id=2");
1438 const GURL kActivationUrl("https://example.com/page.html?id=3");
1439
1440 auto make_nvs_candidate_no_tags = [&](const GURL& url) {
1441 auto candidate =
1442 MakeCandidate(url, blink::mojom::SpeculationAction::kPrefetch,
1443 blink::mojom::SpeculationEagerness::kConservative);
1444 candidate->no_vary_search_hint = network::mojom::NoVarySearch::New(
1445 network::mojom::SearchParamsVariance::NewNoVaryParams({"id"}),
1446 /*vary_on_key_order=*/true);
1447 // No tags specified - candidates->tags remains default ({std::nullopt})
1448 return candidate;
1449 };
1450
1451 std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
1452 candidates.push_back(make_nvs_candidate_no_tags(kPreloadingUrl1));
1453 candidates.push_back(make_nvs_candidate_no_tags(kPreloadingUrl2));
1454
1455 preloading_decider->UpdateSpeculationCandidates(candidates);
1456
1457 // Both candidates should be on standby.
1458 EXPECT_TRUE(preloading_decider->IsOnStandByForTesting(
1459 kPreloadingUrl1, blink::mojom::SpeculationAction::kPrefetch));
1460 EXPECT_TRUE(preloading_decider->IsOnStandByForTesting(
1461 kPreloadingUrl2, blink::mojom::SpeculationAction::kPrefetch));
1462
1463 // Check if tags are merged correctly for a matching URL when no tags exist.
1464 const PreloadingDecider::SpeculationCandidateKey lookup_key{
1465 kActivationUrl, blink::mojom::SpeculationAction::kPrefetch};
1466 const PreloadingPredictor predictor =
1467 preloading_predictor::kUrlPointerDownOnAnchor;
1468
1469 auto merged_tags =
1470 preloading_decider->GetMergedSpeculationTagsFromSuitableCandidates(
Takashi Nakayama80498ee2025-07-31 05:31:311471 lookup_key, predictor, PreloadingConfidence{100},
1472 /*eagerness_to_exclude=*/{});
Rulong Chen(陈汝龙)88e06882025-07-14 12:57:241473
1474 // The merged tags should contain a single std::nullopt since no candidates
1475 // have tags.
1476 EXPECT_EQ(merged_tags.size(), 1u);
1477 EXPECT_TRUE(base::Contains(merged_tags, std::optional<std::string>{}));
1478}
Iman Saboori64dc4de2022-11-29 18:58:061479} // namespace content