LibHTTP: Impose a limit on the total disk cache size

Rather than letting our disk cache grow unbounded, let's impose a limit
on the estimated total disk cache size. The limits chosen are vaguely
inspired by Chromium.

We impose a total disk cache limit of 5 GiB. Chromium imposes an overall
limit of 1.25 GiB; I've chosen more here because we currently cache
uncompressed data from cURL.

The limit is further restricted by the amount of available disk space,
which we just check once at startup (as does Chromium). We will choose a
percentage of the free space available on systems with limited space.

Our eviction errs on the side of simplicity. We will remove the least
recently accessed entries until the total estimated cache size does not
exceed our limit. This could potentially be improved in the future. For
example, if the next entry to consider is 40 MiB, and we only need to
free 1 MiB of space, we could try evicting slightly more recently used
entries. This would prevent evicting more than we need to.
This commit is contained in:
Timothy Flynn 2026-02-04 17:53:00 -05:00 committed by Tim Flynn
parent 5f2063d5d9
commit d773ba25cf
7 changed files with 113 additions and 18 deletions

View file

@ -204,6 +204,7 @@ ErrorOr<void> CacheEntryWriter::flush(NonnullRefPtr<HeaderList> request_headers,
}
m_index.create_entry(m_cache_key, m_vary_key, m_url, move(request_headers), move(response_headers), m_cache_footer.data_size, m_request_time, m_response_time);
m_disk_cache.remove_entries_exceeding_cache_limit();
dbgln_if(HTTP_DISK_CACHE_DEBUG, "\033[36m[disk]\033[0m \033[34;1mFinished caching\033[0m {} ({} bytes)", m_url, m_cache_footer.data_size);
return {};

View file

@ -6,6 +6,7 @@
#include <AK/Debug.h>
#include <AK/StringBuilder.h>
#include <LibFileSystem/FileSystem.h>
#include <LibHTTP/Cache/CacheIndex.h>
#include <LibHTTP/Cache/Utilities.h>
#include <LibHTTP/Cache/Version.h>
@ -103,18 +104,46 @@ ErrorOr<CacheIndex> CacheIndex::create(Database::Database& database)
statements.update_response_headers = TRY(database.prepare_statement("UPDATE CacheIndex SET response_headers = ? WHERE cache_key = ? AND vary_key = ?;"sv));
statements.update_last_access_time = TRY(database.prepare_statement("UPDATE CacheIndex SET last_access_time = ? WHERE cache_key = ? AND vary_key = ?;"sv));
statements.remove_entries_exceeding_cache_limit = TRY(database.prepare_statement(R"#(
WITH RankedCacheIndex AS (
SELECT
cache_key,
vary_key,
SUM(data_size + OCTET_LENGTH(request_headers) + OCTET_LENGTH(response_headers))
OVER (ORDER BY last_access_time DESC)
AS cumulative_estimated_size
FROM CacheIndex
)
DELETE FROM CacheIndex
WHERE (cache_key, vary_key) IN (
SELECT cache_key, vary_key
FROM RankedCacheIndex
WHERE cumulative_estimated_size > ?
)
RETURNING cache_key, vary_key;
)#"sv));
statements.estimate_cache_size_accessed_since = TRY(database.prepare_statement(R"#(
SELECT SUM(data_size + OCTET_LENGTH(request_headers) + OCTET_LENGTH(response_headers))
FROM CacheIndex
WHERE last_access_time >= ?;
)#"sv));
return CacheIndex { database, statements };
auto disk_space = TRY(FileSystem::compute_disk_space(database.database_path().parent()));
auto maximum_disk_cache_size = compute_maximum_disk_cache_size(disk_space.free_bytes);
Limits limits {
.free_disk_space = disk_space.free_bytes,
.maximum_disk_cache_size = maximum_disk_cache_size,
};
return CacheIndex { database, statements, limits };
}
CacheIndex::CacheIndex(Database::Database& database, Statements statements)
CacheIndex::CacheIndex(Database::Database& database, Statements statements, Limits limits)
: m_database(database)
, m_statements(statements)
, m_limits(limits)
{
}
@ -149,7 +178,22 @@ void CacheIndex::create_entry(u64 cache_key, u64 vary_key, String url, NonnullRe
void CacheIndex::remove_entry(u64 cache_key, u64 vary_key)
{
m_database->execute_statement(m_statements.remove_entry, {}, cache_key, vary_key);
m_entries.remove(cache_key);
delete_entry(cache_key, vary_key);
}
void CacheIndex::remove_entries_exceeding_cache_limit(Function<void(u64 cache_key, u64 vary_key)> on_entry_removed)
{
m_database->execute_statement(
m_statements.remove_entries_exceeding_cache_limit,
[&](auto statement_id) {
auto cache_key = m_database->result_column<u64>(statement_id, 0);
auto vary_key = m_database->result_column<u64>(statement_id, 1);
delete_entry(cache_key, vary_key);
if (on_entry_removed)
on_entry_removed(cache_key, vary_key);
},
m_limits.maximum_disk_cache_size);
}
void CacheIndex::remove_entries_accessed_since(UnixDateTime since, Function<void(u64 cache_key, u64 vary_key)> on_entry_removed)
@ -159,15 +203,10 @@ void CacheIndex::remove_entries_accessed_since(UnixDateTime since, Function<void
[&](auto statement_id) {
auto cache_key = m_database->result_column<u64>(statement_id, 0);
auto vary_key = m_database->result_column<u64>(statement_id, 1);
delete_entry(cache_key, vary_key);
if (auto entries = m_entries.get(cache_key); entries.has_value()) {
entries->remove_first_matching([&](auto const& entry) { return entry.vary_key == vary_key; });
if (entries->is_empty())
m_entries.remove(cache_key);
if (on_entry_removed)
on_entry_removed(cache_key, vary_key);
}
},
since);
}
@ -234,6 +273,18 @@ Optional<CacheIndex::Entry&> CacheIndex::get_entry(u64 cache_key, u64 vary_key)
return find_value(*entries, [&](auto const& entry) { return entry.vary_key == vary_key; });
}
void CacheIndex::delete_entry(u64 cache_key, u64 vary_key)
{
auto entries = m_entries.get(cache_key);
if (!entries.has_value())
return;
entries->remove_first_matching([&](auto const& entry) { return entry.vary_key == vary_key; });
if (entries->is_empty())
m_entries.remove(cache_key);
}
Requests::CacheSizes CacheIndex::estimate_cache_size_accessed_since(UnixDateTime since)
{
Requests::CacheSizes sizes;

View file

@ -38,6 +38,7 @@ public:
void create_entry(u64 cache_key, u64 vary_key, String url, NonnullRefPtr<HeaderList> request_headers, NonnullRefPtr<HeaderList> response_headers, u64 data_size, UnixDateTime request_time, UnixDateTime response_time);
void remove_entry(u64 cache_key, u64 vary_key);
void remove_entries_exceeding_cache_limit(Function<void(u64 cache_key, u64 vary_key)> on_entry_removed);
void remove_entries_accessed_since(UnixDateTime, Function<void(u64 cache_key, u64 vary_key)> on_entry_removed);
Optional<Entry const&> find_entry(u64 cache_key, HeaderList const& request_headers);
@ -51,6 +52,7 @@ private:
struct Statements {
Database::StatementID insert_entry { 0 };
Database::StatementID remove_entry { 0 };
Database::StatementID remove_entries_exceeding_cache_limit { 0 };
Database::StatementID remove_entries_accessed_since { 0 };
Database::StatementID select_entries { 0 };
Database::StatementID update_response_headers { 0 };
@ -58,14 +60,22 @@ private:
Database::StatementID estimate_cache_size_accessed_since { 0 };
};
CacheIndex(Database::Database&, Statements);
struct Limits {
u64 free_disk_space { 0 };
u64 maximum_disk_cache_size { 0 };
};
CacheIndex(Database::Database&, Statements, Limits);
Optional<Entry&> get_entry(u64 cache_key, u64 vary_key);
void delete_entry(u64 cache_key, u64 vary_key);
NonnullRawPtr<Database::Database> m_database;
Statements m_statements;
HashMap<u64, Vector<Entry>> m_entries;
Limits m_limits;
};
}

View file

@ -230,6 +230,13 @@ bool DiskCache::check_if_cache_has_open_entry(CacheRequest& request, u64 cache_k
return false;
}
void DiskCache::remove_entries_exceeding_cache_limit()
{
m_index.remove_entries_exceeding_cache_limit([&](auto cache_key, auto vary_key) {
delete_entry(cache_key, vary_key);
});
}
Requests::CacheSizes DiskCache::estimate_cache_size_accessed_since(UnixDateTime since)
{
return m_index.estimate_cache_size_accessed_since(since);
@ -238,13 +245,7 @@ Requests::CacheSizes DiskCache::estimate_cache_size_accessed_since(UnixDateTime
void DiskCache::remove_entries_accessed_since(UnixDateTime since)
{
m_index.remove_entries_accessed_since(since, [&](auto cache_key, auto vary_key) {
if (auto open_entries = m_open_cache_entries.get(cache_key); open_entries.has_value()) {
for (auto const& [open_entry, _] : *open_entries)
open_entry->mark_for_deletion({});
}
auto cache_path = path_for_cache_entry(m_cache_directory, cache_key, vary_key);
(void)FileSystem::remove(cache_path.string(), FileSystem::RecursionMode::Disallowed);
delete_entry(cache_key, vary_key);
});
}
@ -278,4 +279,15 @@ void DiskCache::cache_entry_closed(Badge<CacheEntry>, CacheEntry const& cache_en
}
}
void DiskCache::delete_entry(u64 cache_key, u64 vary_key)
{
if (auto open_entries = m_open_cache_entries.get(cache_key); open_entries.has_value()) {
for (auto const& [open_entry, _] : *open_entries)
open_entry->mark_for_deletion({});
}
auto cache_path = path_for_cache_entry(m_cache_directory, cache_key, vary_key);
(void)FileSystem::remove(cache_path.string(), FileSystem::RecursionMode::Disallowed);
}
}

View file

@ -52,6 +52,8 @@ public:
};
Variant<Optional<CacheEntryReader&>, CacheHasOpenEntry> open_entry(CacheRequest&, URL::URL const&, StringView method, HeaderList const& request_headers, CacheMode, OpenMode);
void remove_entries_exceeding_cache_limit();
Requests::CacheSizes estimate_cache_size_accessed_since(UnixDateTime since);
void remove_entries_accessed_since(UnixDateTime since);
@ -68,6 +70,8 @@ private:
};
bool check_if_cache_has_open_entry(CacheRequest&, u64 cache_key, URL::URL const&, CheckReaderEntries);
void delete_entry(u64 cache_key, u64 vary_key);
Mode m_mode;
NonnullRefPtr<Database::Database> m_database;

View file

@ -24,6 +24,21 @@ static Optional<UnixDateTime> parse_http_date(Optional<ByteString const&> date)
return {};
}
u64 compute_maximum_disk_cache_size(u64 free_bytes)
{
static constexpr u64 MAXIMUM_DISK_CACHE_SIZE = 5 * GiB;
if (free_bytes <= 100 * MiB)
return free_bytes * 8 / 10; // Up to 80 MiB
if (free_bytes <= 800 * MiB)
return free_bytes * 6 / 10; // Up to 480 MiB
if (free_bytes <= 2 * GiB)
return free_bytes * 4 / 10; // Up to 820 MiB
if (free_bytes <= 10 * GiB)
return free_bytes * 2 / 10; // Up to 2 GiB
return MAXIMUM_DISK_CACHE_SIZE;
}
String serialize_url_for_cache_storage(URL::URL const& url)
{
if (!url.fragment().has_value())

View file

@ -21,6 +21,8 @@ constexpr inline auto TEST_CACHE_STATUS_HEADER = "X-Ladybird-Disk-Cache-Status"s
constexpr inline auto TEST_CACHE_REVALIDATION_STATUS_HEADER = "X-Ladybird-Revalidation-Status"sv;
constexpr inline auto TEST_CACHE_REQUEST_TIME_OFFSET = "X-Ladybird-Request-Time-Offset"sv;
u64 compute_maximum_disk_cache_size(u64 free_bytes);
String serialize_url_for_cache_storage(URL::URL const&);
u64 create_cache_key(StringView url, StringView method);
u64 create_vary_key(HeaderList const& request_headers, HeaderList const& response_headers);