| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/preloading/prefetch/prefetch_match_resolver.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/browser/preloading/prefetch/prefetch_container.h" |
| #include "content/browser/preloading/prefetch/prefetch_params.h" |
| #include "content/browser/preloading/prefetch/prefetch_service.h" |
| #include "content/browser/preloading/prerender/prerender_features.h" |
| #include "content/browser/preloading/prerender/prerender_host.h" |
| #include "content/browser/preloading/prerender/prerender_host_registry.h" |
| #include "content/browser/renderer_host/render_frame_host_delegate.h" |
| #include "content/public/browser/navigation_handle_user_data.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Returns keys of `map` for safe iteration. |
| // |
| // `K` must be copyable. |
| template <class K, class V> |
| std::vector<K> Keys(const std::map<K, V>& map) { |
| std::vector<K> keys; |
| |
| for (auto& item : map) { |
| keys.push_back(item.first); |
| } |
| |
| return keys; |
| } |
| |
| } // namespace |
| |
| PrefetchMatchResolver2::CandidateData::CandidateData() = default; |
| PrefetchMatchResolver2::CandidateData::~CandidateData() = default; |
| |
| PrefetchMatchResolver2::PrefetchMatchResolver2( |
| PrefetchContainer::Key navigated_key, |
| base::WeakPtr<PrefetchService> prefetch_service, |
| Callback callback) |
| : navigated_key_(std::move(navigated_key)), |
| prefetch_service_(std::move(prefetch_service)), |
| callback_(std::move(callback)) {} |
| |
| PrefetchMatchResolver2::~PrefetchMatchResolver2() = default; |
| |
| std::optional<base::TimeDelta> PrefetchMatchResolver2::GetBlockedDuration() |
| const { |
| if (wait_started_at_.has_value()) { |
| return base::TimeTicks::Now() - wait_started_at_.value(); |
| } else { |
| return std::nullopt; |
| } |
| } |
| |
| // static |
| void PrefetchMatchResolver2::FindPrefetch( |
| PrefetchContainer::Key navigated_key, |
| bool is_nav_prerender, |
| PrefetchService& prefetch_service, |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container, |
| Callback callback) { |
| TRACE_EVENT0("loading", "PrefetchMatchResolver2::FindPrefetch"); |
| // See the comment of `self_`. |
| auto prefetch_match_resolver = base::WrapUnique(new PrefetchMatchResolver2( |
| std::move(navigated_key), prefetch_service.GetWeakPtr(), |
| std::move(callback))); |
| PrefetchMatchResolver2& ref = *prefetch_match_resolver.get(); |
| ref.self_ = std::move(prefetch_match_resolver); |
| |
| ref.FindPrefetchInternal(is_nav_prerender, prefetch_service, |
| std::move(serving_page_metrics_container)); |
| } |
| |
| void PrefetchMatchResolver2::FindPrefetchInternal( |
| bool is_nav_prerender, |
| PrefetchService& prefetch_service, |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container) { |
| auto [candidates, servable_states] = prefetch_service.CollectMatchCandidates( |
| navigated_key_, is_nav_prerender, |
| std::move(serving_page_metrics_container)); |
| // Consume `candidates`. |
| for (auto& prefetch_container : candidates) { |
| RegisterCandidate(*prefetch_container); |
| } |
| |
| // Backward compatibility to the behavior of `PrefetchMatchResolver`: If it |
| // finds a `PrefetchContainer` in which the cookies of the head of redirect |
| // chain changed, propagate it to other potentially matching |
| // `PrefetchContainer`s and give up to serve. |
| // |
| // TODO(kenoss): kenoss@ believes that we can improve it by propagating |
| // cookies changed event with `PrefetchContainer` and `PrefetchService` |
| // (without `PrefetchMatchResoler`/`PrefetchMatchResoler2`) and marking them |
| // `kNotServable`. |
| for (auto& candidate : candidates_) { |
| switch (servable_states.at(candidate.first)) { |
| case PrefetchContainer::ServableState::kServable: |
| if (candidate.second->prefetch_container->CreateReader() |
| .HaveDefaultContextCookiesChanged()) { |
| UnblockForCookiesChanged(candidate.second->prefetch_container->key()); |
| return; |
| } |
| break; |
| case PrefetchContainer::ServableState::kNotServable: |
| NOTREACHED(); |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot: |
| // nop |
| break; |
| } |
| } |
| |
| for (auto& candidate : candidates_) { |
| switch (servable_states.at(candidate.first)) { |
| case PrefetchContainer::ServableState::kServable: |
| // Got matching and servable. |
| UnblockForMatch(candidate.first); |
| return; |
| case PrefetchContainer::ServableState::kNotServable: |
| NOTREACHED(); |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot: |
| // nop |
| break; |
| } |
| } |
| |
| // There is no matching and servable prefetch at this point. We should wait |
| // remaining ones. |
| |
| CHECK(!wait_started_at_.has_value()); |
| wait_started_at_ = base::TimeTicks::Now(); |
| |
| for (auto& candidate : candidates_) { |
| StartWaitFor(candidate.first, servable_states.at(candidate.first)); |
| } |
| |
| if (candidates_.size() == 0) { |
| UnblockForNoCandidates(); |
| } |
| } |
| |
| void PrefetchMatchResolver2::RegisterCandidate( |
| PrefetchContainer& prefetch_container) { |
| auto candidate_data = std::make_unique<CandidateData>(); |
| // #prefetch-key-availability |
| // |
| // Note that `CHECK(candidates_.contains(prefetch_key))` and |
| // `CHECK(candidate_data->prefetch_container)` below always hold because |
| // `PrefetchMatchResolver2` observes lifecycle events of `PrefetchContainer`. |
| candidate_data->prefetch_container = prefetch_container.GetWeakPtr(); |
| candidate_data->timeout_timer = nullptr; |
| |
| candidates_[prefetch_container.key()] = std::move(candidate_data); |
| } |
| |
| void PrefetchMatchResolver2::StartWaitFor( |
| const PrefetchContainer::Key& prefetch_key, |
| PrefetchContainer::ServableState servable_state) { |
| // By #prefetch-key-availability |
| CHECK(candidates_.contains(prefetch_key)); |
| auto& candidate_data = candidates_[prefetch_key]; |
| CHECK(candidate_data->prefetch_container); |
| PrefetchContainer& prefetch_container = *candidate_data->prefetch_container; |
| |
| // `kServable` -> `kNotServable` is the only possible change during |
| // `FindPrefetchInternal()` call. |
| CHECK_EQ(prefetch_container.GetServableState(PrefetchCacheableDuration()), |
| servable_state); |
| switch (servable_state) { |
| case PrefetchContainer::ServableState::kServable: |
| case PrefetchContainer::ServableState::kNotServable: |
| NOTREACHED(); |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot: |
| // nop |
| break; |
| } |
| CHECK(!candidate_data->timeout_timer); |
| |
| // TODO(crbug.com/356552413): Merge |
| // https://chromium-review.googlesource.com/c/chromium/src/+/5668924 and write |
| // tests. |
| base::TimeDelta timeout = |
| PrefetchBlockUntilHeadTimeout(prefetch_container.GetPrefetchType()); |
| if (timeout.is_positive()) { |
| candidate_data->timeout_timer = std::make_unique<base::OneShotTimer>(); |
| candidate_data->timeout_timer->Start( |
| FROM_HERE, timeout, |
| base::BindOnce(&PrefetchMatchResolver2::OnTimeout, |
| // Safety: `timeout_timer` is owned by this. |
| Unretained(this), prefetch_key)); |
| } |
| |
| prefetch_container.AddObserver(this); |
| } |
| |
| void PrefetchMatchResolver2::UnregisterCandidate( |
| const PrefetchContainer::Key& prefetch_key, |
| bool is_served) { |
| // By #prefetch-key-availability |
| CHECK(candidates_.contains(prefetch_key)); |
| auto& candidate_data = candidates_[prefetch_key]; |
| CHECK(candidate_data->prefetch_container); |
| PrefetchContainer& prefetch_container = *candidate_data->prefetch_container; |
| |
| prefetch_container.OnUnregisterCandidate(navigated_key_.url(), is_served, |
| GetBlockedDuration()); |
| prefetch_container.RemoveObserver(this); |
| candidates_.erase(prefetch_key); |
| } |
| |
| void PrefetchMatchResolver2::OnWillBeDestroyed( |
| PrefetchContainer& prefetch_container) { |
| MaybeUnblockForUnmatch(prefetch_container.key()); |
| } |
| |
| void PrefetchMatchResolver2::OnGotInitialEligibility( |
| PrefetchContainer& prefetch_container, |
| PreloadingEligibility eligibility) { |
| CHECK(base::FeatureList::IsEnabled( |
| features::kPrerender2FallbackPrefetchSpecRules)); |
| |
| if (eligibility != PreloadingEligibility::kEligible) { |
| MaybeUnblockForUnmatch(prefetch_container.key()); |
| } |
| } |
| |
| void PrefetchMatchResolver2::OnDeterminedHead( |
| PrefetchContainer& prefetch_container) { |
| CHECK(candidates_.contains(prefetch_container.key())); |
| CHECK(!prefetch_container.is_in_dtor()); |
| |
| // Note that `OnDeterimnedHead()` is called even if `PrefetchContainer` is in |
| // failure `PrefetchState`. See, for example, https://crbug.com/375333786. |
| switch (prefetch_container.GetServableState(PrefetchCacheableDuration())) { |
| case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot: |
| // All callsites of `PrefetchContainer::OnDeterminedHead2()` are |
| // `PrefetchStreamingURLLoader`, which implies the prefetch passed |
| // eligibility check. |
| NOTREACHED(); |
| // `kShouldBlockUntilHeadReceived` case occurs if a prefetch is redirected |
| // and the redirect is not eligible. |
| // |
| // PrefetchService::OnGotEligibilityForRedirect() |
| // -> PrefetchStreamingURLLoader::HandleRedirect(kFail) |
| // -> PrefetchContainer::OnDeterminedHead2() |
| // -> here |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| case PrefetchContainer::ServableState::kNotServable: |
| MaybeUnblockForUnmatch(prefetch_container.key()); |
| return; |
| case PrefetchContainer::ServableState::kServable: |
| // Proceed. |
| break; |
| } |
| |
| if (prefetch_container.CreateReader().HaveDefaultContextCookiesChanged()) { |
| UnblockForCookiesChanged(prefetch_container.key()); |
| return; |
| } |
| |
| // Non-redirect header is received and now the value of |
| // `PrefetchContainer::IsNoVarySearchHeaderMatch()` is determined. |
| const bool is_match = |
| prefetch_container.IsExactMatch(navigated_key_.url()) || |
| prefetch_container.IsNoVarySearchHeaderMatch(navigated_key_.url()); |
| if (!is_match) { |
| MaybeUnblockForUnmatch(prefetch_container.key()); |
| return; |
| } |
| |
| if (prefetch_container.HasPrefetchBeenConsideredToServe()) { |
| MaybeUnblockForUnmatch(prefetch_container.key()); |
| return; |
| } |
| |
| // Got matching and servable. |
| UnblockForMatch(prefetch_container.key()); |
| } |
| |
| void PrefetchMatchResolver2::OnTimeout(PrefetchContainer::Key prefetch_key) { |
| // `timeout_timer` is alive, which implies `candidate` is alive. |
| CHECK(candidates_.contains(prefetch_key)); |
| auto& candidate_data = candidates_[prefetch_key]; |
| CHECK(candidate_data->prefetch_container); |
| |
| MaybeUnblockForUnmatch(prefetch_key); |
| } |
| |
| void PrefetchMatchResolver2::UnblockForMatch( |
| const PrefetchContainer::Key& prefetch_key) { |
| TRACE_EVENT0("loading", "PrefetchMatchResolver2::UnblockForMatch"); |
| |
| // By #prefetch-key-availability |
| CHECK(candidates_.contains(prefetch_key)); |
| auto& candidate_data = candidates_[prefetch_key]; |
| CHECK(candidate_data->prefetch_container); |
| PrefetchContainer& prefetch_container = *candidate_data->prefetch_container; |
| |
| UnregisterCandidate(prefetch_key, /*is_served=*/true); |
| |
| // Unregister remaining candidates as not served. |
| for (auto& key2 : Keys(candidates_)) { |
| UnregisterCandidate(key2, /*is_served=*/false); |
| } |
| |
| // Postprocess for success case. |
| |
| PrefetchContainer::Reader reader = prefetch_container.CreateReader(); |
| |
| // Cookie change is handled in two paths: |
| // |
| // - Before waiting `PrefetchContainer`, which is `kServable` at the match |
| // start timing. It is handled in `FindPrefetchInternal()`. |
| // - After waiting `PrefetchContainer`, which is `kShouldBlockUntilHead` at |
| // the match start timing. It is handled in `OnDeterminedHead()`. |
| // |
| // So, the below condition is satisfied. |
| CHECK(!reader.HaveDefaultContextCookiesChanged()); |
| |
| if (!reader.HasIsolatedCookieCopyStarted()) { |
| // Basically, we can assume `PrefetchService` is available as waiting |
| // `PrefetchContainer` is owned by it. But in unit tests, we use invalid |
| // frame tree node id and this `prefetch_service` is not available. |
| if (prefetch_service_) { |
| prefetch_service_->CopyIsolatedCookies(reader); |
| } |
| } |
| CHECK(reader); |
| |
| UnblockInternal(std::move(reader)); |
| } |
| |
| void PrefetchMatchResolver2::UnblockForNoCandidates() { |
| TRACE_EVENT0("loading", "PrefetchMatchResolver2::UnblockForNoCandidates"); |
| UnblockInternal({}); |
| } |
| |
| void PrefetchMatchResolver2::MaybeUnblockForUnmatch( |
| const PrefetchContainer::Key& prefetch_key) { |
| UnregisterCandidate(prefetch_key, /*is_served=*/false); |
| |
| if (candidates_.size() == 0) { |
| UnblockForNoCandidates(); |
| } |
| |
| // It still waits for other `PrefetchContainer`s. |
| } |
| |
| void PrefetchMatchResolver2::UnblockForCookiesChanged( |
| const PrefetchContainer::Key& key) { |
| // Unregister remaining candidates as not served, with calling |
| // `PrefetchContainer::OnDetectedCookiesChange2()`. |
| for (auto& prefetch_key : Keys(candidates_)) { |
| // By #prefetch-key-availability |
| CHECK(candidates_.contains(prefetch_key)); |
| auto& candidate_data = candidates_[prefetch_key]; |
| CHECK(candidate_data->prefetch_container); |
| PrefetchContainer& prefetch_container = *candidate_data->prefetch_container; |
| |
| UnregisterCandidate(prefetch_key, /*is_served=*/false); |
| |
| prefetch_container.OnDetectedCookiesChange2( |
| /*is_unblock_for_cookies_changed_triggered_by_this_prefetch_container*/ |
| prefetch_key == key); |
| } |
| |
| UnblockForNoCandidates(); |
| } |
| |
| void PrefetchMatchResolver2::UnblockInternal(PrefetchContainer::Reader reader) { |
| // Postcondition: This resolver waits for no `PrefetchContainer`s when it has |
| // been unblocking. |
| CHECK_EQ(candidates_.size(), 0u); |
| |
| auto callback = std::move(callback_); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE, |
| std::move(self_)); |
| |
| std::move(callback).Run(std::move(reader)); |
| } |
| |
| } // namespace content |