| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/prefetch_cache.h" |
| |
| #include <ostream> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/types/pass_key.h" |
| #include "net/base/network_isolation_key.h" |
| #include "services/network/prefetch_url_loader_client.h" |
| #include "services/network/public/cpp/features.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| size_t GetMaxSize() { |
| // How many prefetches should be cached before old ones are evicted. This |
| // provides rough control over the overall memory used by prefetches. |
| return static_cast<size_t>(std::max( |
| base::GetFieldTrialParamByFeatureAsInt(features::kNetworkContextPrefetch, |
| /*name=*/"max_loaders", |
| /*default_value=*/10), |
| 1)); |
| } |
| |
| base::TimeDelta GetEraseGraceTime() { |
| // When "NetworkContextPrefetchUseMatches" is disabled, how long to leave a |
| // matched cache entry alive before deleting it. This corresponds to the |
| // expected maximum time it will take for a request to reach the HttpCache |
| // once it has been initiated. Since it may be delayed by the |
| // ResourceScheduler, give the delay is quite large. |
| // |
| // Why not shorter: the request from the render process may be delayed by the |
| // ResourceScheduler. |
| // |
| // Why not longer: If the prefetch has not yet received response headers, it |
| // has an exclusive cache lock. The real request from the render process |
| // cannot proceed until the cache lock is released. If the response turns out |
| // to be uncacheable, then this time is pure waste. |
| return std::max(base::Seconds(0), |
| base::GetFieldTrialParamByFeatureAsTimeDelta( |
| features::kNetworkContextPrefetch, |
| /*name=*/"erase_grace_time", |
| /*default_value=*/base::Seconds(1))); |
| } |
| |
| } // namespace |
| |
| PrefetchCache::PrefetchCache() |
| : max_size_(GetMaxSize()), erase_grace_time_(GetEraseGraceTime()) {} |
| |
| PrefetchCache::~PrefetchCache() = default; |
| |
| PrefetchURLLoaderClient* PrefetchCache::Emplace( |
| const ResourceRequest& request) { |
| if (!request.trusted_params.has_value()) { |
| DLOG(WARNING) |
| << "NetworkContext::Emplace() was called with a request with no " |
| "NetworkIsolationKey. This is not going to work."; |
| return nullptr; |
| } |
| |
| const net::NetworkIsolationKey& nik = |
| request.trusted_params->isolation_info.network_isolation_key(); |
| |
| if (nik.IsTransient()) { |
| DLOG(WARNING) << "NetworkContext::Emplace() was called with a request with " |
| "a transient NetworkIsolationKey. This won't match " |
| "anything, so ignoring."; |
| return nullptr; |
| } |
| |
| if (!request.url.SchemeIsHTTPOrHTTPS()) { |
| DLOG(WARNING) << "NetworkContext::Emplace() was called with a scheme that " |
| "is not http or https. This is not going to work."; |
| return nullptr; |
| } |
| |
| if (map_.contains(KeyType(nik, request.url))) { |
| return nullptr; |
| } |
| |
| while (map_.size() >= max_size_) { |
| EraseOldest(); |
| } |
| |
| auto [it, insert_ok] = |
| client_storage_.insert(std::make_unique<PrefetchURLLoaderClient>( |
| base::PassKey<PrefetchCache>(), nik, request, |
| /*expiry_time=*/base::TimeTicks::Now() + kMaxAge, this)); |
| CHECK(insert_ok); |
| auto* client = it->get(); |
| |
| list_.Append(client); |
| auto [_, is_not_duplicated] = map_.emplace( |
| KeyType(client->network_isolation_key(), client->url()), client); |
| CHECK(is_not_duplicated); |
| |
| if (!expiry_timer_.IsRunning()) { |
| StartTimer(); |
| } |
| |
| return client; |
| } |
| |
| PrefetchURLLoaderClient* PrefetchCache::Lookup( |
| const net::NetworkIsolationKey& nik, |
| const GURL& url) { |
| const auto it = FindInMap(nik, url); |
| return it == map_.end() ? nullptr : it->second; |
| } |
| |
| void PrefetchCache::Consume(PrefetchURLLoaderClient* client) { |
| const bool was_oldest = client == list_.head(); |
| RemoveFromCache(client); |
| if (was_oldest) { |
| if (list_.empty()) { |
| expiry_timer_.Stop(); |
| } else { |
| // Automatically resets the timer. |
| StartTimer(); |
| } |
| } |
| } |
| |
| void PrefetchCache::Erase(PrefetchURLLoaderClient* client) { |
| const auto it = FindInMap(client->network_isolation_key(), client->url()); |
| if (it != map_.end()) { |
| CHECK_EQ(it->second, client); |
| map_.erase(it); |
| client->RemoveFromList(); |
| } |
| EraseFromStorage(client); |
| } |
| |
| void PrefetchCache::DelayedErase(PrefetchURLLoaderClient* client) { |
| const auto now = base::TimeTicks::Now(); |
| PendingErasure pending_erasure = {client->network_isolation_key(), |
| client->url(), now + erase_grace_time_}; |
| CHECK(Lookup(pending_erasure.nik, pending_erasure.url)); |
| delayed_erase_queue_.push(std::move(pending_erasure)); |
| SchedulePendingErases(now); |
| } |
| |
| void PrefetchCache::OnTimer() { |
| auto now = base::TimeTicks::Now(); |
| while (!list_.empty() && |
| list_.head()->value()->expiry_time() <= now + kExpirySlack) { |
| EraseOldest(); |
| } |
| if (!list_.empty()) { |
| StartTimer(now); |
| } |
| } |
| |
| void PrefetchCache::DoDelayedErases() { |
| const auto now = base::TimeTicks::Now(); |
| while (!delayed_erase_queue_.empty() && |
| delayed_erase_queue_.front().erase_time <= now) { |
| PendingErasure pending = std::move(delayed_erase_queue_.front()); |
| delayed_erase_queue_.pop(); |
| PrefetchURLLoaderClient* to_erase = Lookup(pending.nik, pending.url); |
| if (to_erase) { |
| RemoveFromCache(to_erase); |
| EraseFromStorage(to_erase); |
| } |
| } |
| if (!delayed_erase_queue_.empty()) { |
| SchedulePendingErases(now); |
| } |
| } |
| |
| void PrefetchCache::EraseOldest() { |
| CHECK(!list_.empty()); |
| PrefetchURLLoaderClient* oldest = list_.head()->value(); |
| RemoveFromCache(oldest); |
| // Make sure we actually removed the right one. |
| CHECK_NE(oldest, list_.head()); |
| EraseFromStorage(oldest); |
| } |
| |
| void PrefetchCache::RemoveFromCache(PrefetchURLLoaderClient* client) { |
| const auto it = FindInMap(client->network_isolation_key(), client->url()); |
| CHECK(it != map_.end()); |
| CHECK_EQ(it->second, client); |
| map_.erase(it); |
| client->RemoveFromList(); |
| } |
| |
| void PrefetchCache::EraseFromStorage(PrefetchURLLoaderClient* client) { |
| // In C++20, std::set::erase() doesn't support transparent comparisons, so |
| // it's necessary to use find() first. |
| auto it = client_storage_.find(client); |
| CHECK(it != client_storage_.end()); |
| client_storage_.erase(it); |
| } |
| |
| void PrefetchCache::StartTimer(base::TimeTicks now) { |
| CHECK(!list_.empty()); |
| auto next_expiry = list_.head()->value()->expiry_time(); |
| // It's safe to use base::Unretained() as destroying `this` will destroy |
| // `expiry_timer_`, preventing the callback being called. |
| expiry_timer_.Start( |
| FROM_HERE, std::max(base::Seconds(0), next_expiry - now), |
| base::BindOnce(&PrefetchCache::OnTimer, base::Unretained(this))); |
| } |
| |
| void PrefetchCache::SchedulePendingErases(base::TimeTicks now) { |
| CHECK(!delayed_erase_queue_.empty()); |
| const auto next_expiry = delayed_erase_queue_.front().erase_time; |
| const auto delay = std::max(base::Seconds(0), next_expiry - now); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&PrefetchCache::DoDelayedErases, |
| weak_factory_.GetWeakPtr()), |
| delay); |
| } |
| |
| PrefetchCache::MapType::iterator PrefetchCache::FindInMap( |
| const net::NetworkIsolationKey& nik, |
| const GURL& url) { |
| return map_.find(KeyType(nik, url)); |
| } |
| |
| } // namespace network |