blob: f9d829179b4dd9074bc1f40e1881880cff93a044 [file] [log] [blame]
// 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