LibWeb: Preserve JS bytecode in memory cache

Store JavaScript bytecode side data in the WebContent HTTP memory
cache and replay it when serving cached responses. Also update an
already-complete memory-cache entry when asynchronous bytecode cache
generation finishes, so the first source-only response does not keep
shadowing the disk-cache sidecar during same-process navigations.

Keep the HTTP memory-cache backfill keyed with the request headers that
populated the memory-cache entry, so Vary responses still receive their
generated bytecode sidecar.

Add LibHTTP coverage for round-tripping bytecode side data through a
memory-cache entry, attaching it after the response body has already
been cached, and matching Vary headers during updates. Add LibWeb
coverage for preserving the memory-cache request headers when cloning
responses.
This commit is contained in:
Andreas Kling 2026-05-28 01:43:13 +02:00 committed by Andreas Kling
parent 221219e00c
commit 72720bc229
11 changed files with 242 additions and 5 deletions

View file

@ -15,6 +15,14 @@ NonnullRefPtr<MemoryCache> MemoryCache::create()
return adopt_ref(*new MemoryCache());
}
// A stored response satisfies a request only if the request header fields nominated by the response's Vary header
// match those of the request that produced the entry, per RFC 9111 4.1. open_entry() and update_javascript_bytecode_cache()
// must select entries identically, so share the predicate.
static bool entry_matches_request(HeaderList const& request_headers, MemoryCache::Entry const& entry)
{
return create_vary_key(request_headers, entry.response_headers) == entry.vary_key;
}
// https://httpwg.org/specs/rfc9111.html#constructing.responses.from.caches
Optional<MemoryCache::Entry const&> MemoryCache::open_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, CacheMode cache_mode)
{
@ -38,7 +46,7 @@ Optional<MemoryCache::Entry const&> MemoryCache::open_entry(URL::URL const& url,
// - request header fields nominated by the stored response (if any) match those presented (see Section 4.1), and
auto cache_entry = find_value(*cache_entries, [&](auto const& entry) {
return create_vary_key(request_headers, entry.response_headers) == entry.vary_key;
return entry_matches_request(request_headers, entry);
});
if (!cache_entry.has_value()) {
dbgln_if(HTTP_MEMORY_CACHE_DEBUG, "\033[37m[memory]\033[0m \033[35;1mVary mismatch for\033[0m {}", url);
@ -75,7 +83,7 @@ Optional<MemoryCache::Entry const&> MemoryCache::open_entry(URL::URL const& url,
VERIFY_NOT_REACHED();
}
void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers)
void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers, Optional<Core::ImmutableBytes> javascript_bytecode_cache, Optional<u64> javascript_bytecode_cache_vary_key)
{
if (!is_cacheable(method, request_headers))
return;
@ -99,6 +107,8 @@ void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderLis
.request_headers = move(request_headers_copy),
.response_headers = move(response_headers_copy),
.response_body = {},
.javascript_bytecode_cache = move(javascript_bytecode_cache),
.javascript_bytecode_cache_vary_key = javascript_bytecode_cache_vary_key,
.request_time = request_time,
.response_time = UnixDateTime::now(),
};
@ -139,4 +149,33 @@ void MemoryCache::finalize_entry(URL::URL const& url, StringView method, HeaderL
}
}
void MemoryCache::update_javascript_bytecode_cache(URL::URL const& url, StringView method, HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache)
{
if (!is_cacheable(method, request_headers))
return;
auto serialized_url = serialize_url_for_cache_storage(url);
auto cache_key = create_cache_key(serialized_url, method);
// Attach the generated bytecode to every cache entry this request would select. Match entries the same way
// open_entry() does, by re-deriving the vary key from the request and the entry's stored response headers, rather
// than trusting the supplied vary key (which was computed in another process and can diverge for Vary responses).
// The supplied vary key is still kept as javascript_bytecode_cache_vary_key so it can be replayed onto a served
// response.
auto update_entries = [&](Vector<Entry>& entries) {
for (auto& entry : entries) {
if (!entry_matches_request(request_headers, entry))
continue;
entry.javascript_bytecode_cache = javascript_bytecode_cache;
entry.javascript_bytecode_cache_vary_key = vary_key;
}
};
if (auto cache_entries = m_complete_entries.get(cache_key); cache_entries.has_value())
update_entries(*cache_entries);
if (auto cache_entries = m_pending_entries.get(cache_key); cache_entries.has_value())
update_entries(*cache_entries);
}
}

View file

@ -28,6 +28,8 @@ public:
NonnullRefPtr<HeaderList> request_headers;
NonnullRefPtr<HeaderList> response_headers;
Core::ImmutableBytes response_body;
Optional<Core::ImmutableBytes> javascript_bytecode_cache;
Optional<u64> javascript_bytecode_cache_vary_key;
UnixDateTime request_time;
UnixDateTime response_time;
@ -37,8 +39,9 @@ public:
Optional<Entry const&> open_entry(URL::URL const&, StringView method, HeaderList const& request_headers, CacheMode);
void create_entry(URL::URL const&, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers);
void create_entry(URL::URL const&, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers, Optional<Core::ImmutableBytes> javascript_bytecode_cache = {}, Optional<u64> javascript_bytecode_cache_vary_key = {});
void finalize_entry(URL::URL const&, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, Core::ImmutableBytes response_body);
void update_javascript_bytecode_cache(URL::URL const&, StringView method, HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache);
private:
HashMap<u64, Vector<Entry>, IdentityHashTraits<u64>> m_pending_entries;

View file

@ -93,6 +93,14 @@ public:
});
}
HTTP::MemoryCache* get_if_exists(Infrastructure::NetworkPartitionKey const& key)
{
auto it = m_cache.find(key);
if (it == m_cache.end())
return nullptr;
return it->value.ptr();
}
static HTTPCache& the()
{
static HTTPCache& cache = *new HTTPCache;
@ -138,6 +146,9 @@ static GC::Ptr<Infrastructure::Response> select_response_from_cache(JS::Realm& r
response->set_status(cache_entry->status_code);
response->set_status_message(cache_entry->reason_phrase);
response->set_header_list(cache_entry->response_headers);
response->set_javascript_bytecode_cache(cache_entry->javascript_bytecode_cache);
response->set_javascript_bytecode_cache_vary_key(cache_entry->javascript_bytecode_cache_vary_key);
response->set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create(cache_entry->request_headers->headers()));
auto [response_body, _] = safely_extract_body(realm, cache_entry->response_body.bytes());
response->set_body(response_body);
@ -145,14 +156,15 @@ static GC::Ptr<Infrastructure::Response> select_response_from_cache(JS::Realm& r
return response;
}
static void store_response_in_cache(HTTP::MemoryCache& http_cache, Infrastructure::Request const& request, Infrastructure::Response const& response)
static void store_response_in_cache(HTTP::MemoryCache& http_cache, Infrastructure::Request const& request, Infrastructure::Response& response)
{
if (!g_http_memory_cache_enabled)
return;
if (request.cache_mode() == HTTP::CacheMode::NoStore)
return;
http_cache.create_entry(request.current_url(), request.method(), request.header_list(), request.request_time(), response.status(), response.status_message(), response.header_list());
response.set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create(request.header_list()->headers()));
http_cache.create_entry(request.current_url(), request.method(), request.header_list(), request.request_time(), response.status(), response.status_message(), response.header_list(), response.javascript_bytecode_cache(), response.javascript_bytecode_cache_vary_key());
}
// https://fetch.spec.whatwg.org/#concept-fetch
@ -2610,4 +2622,14 @@ void clear_http_memory_cache()
HTTPCache::the().clear_cache();
}
void update_javascript_bytecode_cache_in_http_memory_cache(Infrastructure::NetworkPartitionKey const& partition_key, URL::URL const& url, ByteString const& method, HTTP::HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache)
{
if (!g_http_memory_cache_enabled)
return;
// Only back-fill into a partition that already has a cache; do not create an empty one just to drop bytecode into.
if (auto* http_cache = HTTPCache::the().get_if_exists(partition_key))
http_cache->update_javascript_bytecode_cache(url, method, request_headers, vary_key, move(javascript_bytecode_cache));
}
}

View file

@ -9,11 +9,14 @@
#include <AK/Forward.h>
#include <AK/RefPtr.h>
#include <LibCore/ImmutableBytes.h>
#include <LibGC/Ptr.h>
#include <LibHTTP/Cookie/IncludeCredentials.h>
#include <LibHTTP/Forward.h>
#include <LibJS/Forward.h>
#include <LibURL/Forward.h>
#include <LibWeb/Export.h>
#include <LibWeb/Fetch/Infrastructure/NetworkPartitionKey.h>
#include <LibWeb/Forward.h>
namespace Web::Fetch::Fetching {
@ -60,5 +63,6 @@ void append_fetch_metadata_headers_for_request(Infrastructure::Request&);
WEB_API void set_http_memory_cache_enabled(bool enabled);
WEB_API bool http_memory_cache_enabled();
WEB_API void clear_http_memory_cache();
void update_javascript_bytecode_cache_in_http_memory_cache(Infrastructure::NetworkPartitionKey const&, URL::URL const&, ByteString const& method, HTTP::HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes);
}

View file

@ -182,6 +182,8 @@ GC::Ref<Response> Response::clone(JS::Realm& realm) const
new_response->set_body_info(m_body_info);
new_response->set_javascript_bytecode_cache(m_javascript_bytecode_cache);
new_response->set_javascript_bytecode_cache_vary_key(m_javascript_bytecode_cache_vary_key);
if (m_javascript_bytecode_cache_memory_cache_request_headers.has_value())
new_response->set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create((*m_javascript_bytecode_cache_memory_cache_request_headers)->headers()));
// FIXME: service worker timing info
// 3. If responses body is non-null, then set newResponses body to the result of cloning responses body.

View file

@ -110,6 +110,8 @@ public:
void set_javascript_bytecode_cache(Optional<Core::ImmutableBytes> javascript_bytecode_cache) { m_javascript_bytecode_cache = move(javascript_bytecode_cache); }
[[nodiscard]] Optional<u64> javascript_bytecode_cache_vary_key() const { return m_javascript_bytecode_cache_vary_key; }
void set_javascript_bytecode_cache_vary_key(Optional<u64> javascript_bytecode_cache_vary_key) { m_javascript_bytecode_cache_vary_key = javascript_bytecode_cache_vary_key; }
[[nodiscard]] Optional<NonnullRefPtr<HTTP::HeaderList>> const& javascript_bytecode_cache_memory_cache_request_headers() const { return m_javascript_bytecode_cache_memory_cache_request_headers; }
void set_javascript_bytecode_cache_memory_cache_request_headers(Optional<NonnullRefPtr<HTTP::HeaderList>> request_headers) { m_javascript_bytecode_cache_memory_cache_request_headers = move(request_headers); }
[[nodiscard]] RedirectTaint redirect_taint() const { return m_redirect_taint; }
void set_redirect_taint(RedirectTaint redirect_taint) { m_redirect_taint = redirect_taint; }
@ -203,6 +205,7 @@ private:
Optional<String> m_network_error_message;
Optional<Core::ImmutableBytes> m_javascript_bytecode_cache;
Optional<u64> m_javascript_bytecode_cache_vary_key;
Optional<NonnullRefPtr<HTTP::HeaderList>> m_javascript_bytecode_cache_memory_cache_request_headers;
public:
[[nodiscard]] ByteString const& method() const { return m_method; }

View file

@ -33,6 +33,7 @@
#include <LibWeb/Fetch/Infrastructure/HTTP/MIME.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/Fetch/Infrastructure/NetworkPartitionKey.h>
#include <LibWeb/Fetch/Infrastructure/URL.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
@ -59,7 +60,9 @@ struct BytecodeCacheContext {
URL::URL url;
ByteString method;
NonnullRefPtr<HTTP::HeaderList> request_headers;
RefPtr<HTTP::HeaderList> memory_cache_request_headers;
u64 vary_key { 0 };
Optional<Fetch::Infrastructure::NetworkPartitionKey> memory_cache_partition_key;
};
using BytecodeCacheSourceHash = ::Crypto::Hash::Digest<::Crypto::Hash::SHA256::DigestSize * 8>;
@ -156,11 +159,17 @@ static Optional<BytecodeCacheContext> bytecode_cache_context_for_request(Fetch::
if (!vary_key.has_value())
return {};
RefPtr<HTTP::HeaderList> memory_cache_request_headers;
if (auto const& response_request_headers = response.javascript_bytecode_cache_memory_cache_request_headers(); response_request_headers.has_value())
memory_cache_request_headers = HTTP::HeaderList::create((*response_request_headers)->headers());
return BytecodeCacheContext {
.url = response_url,
.method = request.method(),
.request_headers = HTTP::HeaderList::create(request.header_list()->headers()),
.memory_cache_request_headers = move(memory_cache_request_headers),
.vary_key = *vary_key,
.memory_cache_partition_key = Fetch::Infrastructure::determine_the_network_partition_key(request),
};
}
@ -188,6 +197,8 @@ static void schedule_bytecode_cache_generation(NonnullRefPtr<JS::SourceCode cons
if (!ResourceLoader::is_initialized() || !ResourceLoader::the().request_client())
return;
(void)ResourceLoader::the().request_client()->store_cache_associated_data(cache_context.url, cache_context.method, *cache_context.request_headers, cache_context.vary_key, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, immutable_blob.bytes());
if (cache_context.memory_cache_partition_key.has_value() && cache_context.memory_cache_request_headers)
Fetch::Fetching::update_javascript_bytecode_cache_in_http_memory_cache(*cache_context.memory_cache_partition_key, cache_context.url, cache_context.method, *cache_context.memory_cache_request_headers, cache_context.vary_key, immutable_blob);
});
Threading::ThreadPool::the().submit([filename = move(filename), source_code = move(source_code), type, line_number_offset, callback, &main_thread_event_loop, source_hash]() mutable {

View file

@ -11,3 +11,4 @@ endforeach()
ladybird_test("TestCacheIndex.cpp" LibWeb LIBS LibHTTP LibDatabase)
ladybird_test("TestDiskCache.cpp" LibWeb LIBS LibHTTP LibURL)
ladybird_test("TestMemoryCache.cpp" LibWeb LIBS LibHTTP LibURL)

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/ImmutableBytes.h>
#include <LibHTTP/Cache/MemoryCache.h>
#include <LibHTTP/Cache/Utilities.h>
#include <LibHTTP/HeaderList.h>
#include <LibTest/TestCase.h>
#include <LibURL/Parser.h>
static URL::URL parse_url(StringView url)
{
return URL::Parser::basic_parse(url).release_value();
}
static NonnullRefPtr<HTTP::HeaderList> create_cacheable_request_headers()
{
return HTTP::HeaderList::create({
{ "Accept"sv, "*/*"sv },
});
}
static NonnullRefPtr<HTTP::HeaderList> create_cacheable_response_headers()
{
return HTTP::HeaderList::create({
{ "Cache-Control"sv, "max-age=60"sv },
});
}
static NonnullRefPtr<HTTP::HeaderList> create_vary_user_agent_response_headers()
{
return HTTP::HeaderList::create({
{ "Cache-Control"sv, "max-age=60"sv },
{ "Vary"sv, "User-Agent"sv },
});
}
static Core::ImmutableBytes immutable_bytes(StringView bytes)
{
return MUST(Core::ImmutableBytes::copy(bytes.bytes()));
}
TEST_CASE(javascript_bytecode_cache_round_trips_with_memory_cache_entry)
{
auto cache = HTTP::MemoryCache::create();
auto url = parse_url("https://example.com/script.js"sv);
auto request_headers = create_cacheable_request_headers();
auto response_headers = create_cacheable_response_headers();
auto bytecode = immutable_bytes("cached bytecode"sv);
cache->create_entry(url, "GET"sv, *request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers, bytecode, 0);
cache->finalize_entry(url, "GET"sv, *request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv));
auto entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default);
VERIFY(entry.has_value());
VERIFY(entry->javascript_bytecode_cache.has_value());
EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes());
EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional<u64> { 0 });
}
TEST_CASE(javascript_bytecode_cache_update_matches_memory_cache_vary_headers)
{
auto cache = HTTP::MemoryCache::create();
auto url = parse_url("https://example.com/script.js"sv);
auto script_request_headers = create_cacheable_request_headers();
auto cache_request_headers = HTTP::HeaderList::create({
{ "Accept"sv, "*/*"sv },
{ "User-Agent"sv, "Ladybird"sv },
});
auto response_headers = create_vary_user_agent_response_headers();
auto bytecode = immutable_bytes("generated bytecode"sv);
auto vary_key = HTTP::create_vary_key(*cache_request_headers, *response_headers);
cache->create_entry(url, "GET"sv, *cache_request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers);
cache->finalize_entry(url, "GET"sv, *cache_request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv));
cache->update_javascript_bytecode_cache(url, "GET"sv, *script_request_headers, vary_key, bytecode);
auto entry = cache->open_entry(url, "GET"sv, *cache_request_headers, HTTP::CacheMode::Default);
VERIFY(entry.has_value());
EXPECT(!entry->javascript_bytecode_cache.has_value());
EXPECT(!entry->javascript_bytecode_cache_vary_key.has_value());
cache->update_javascript_bytecode_cache(url, "GET"sv, *cache_request_headers, vary_key, bytecode);
entry = cache->open_entry(url, "GET"sv, *cache_request_headers, HTTP::CacheMode::Default);
VERIFY(entry.has_value());
VERIFY(entry->javascript_bytecode_cache.has_value());
EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes());
EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional<u64> { vary_key });
}
TEST_CASE(javascript_bytecode_cache_can_be_added_after_memory_cache_entry_is_complete)
{
auto cache = HTTP::MemoryCache::create();
auto url = parse_url("https://example.com/script.js"sv);
auto request_headers = create_cacheable_request_headers();
auto response_headers = create_cacheable_response_headers();
auto bytecode = immutable_bytes("generated bytecode"sv);
cache->create_entry(url, "GET"sv, *request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers);
cache->finalize_entry(url, "GET"sv, *request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv));
auto entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default);
VERIFY(entry.has_value());
EXPECT(!entry->javascript_bytecode_cache.has_value());
EXPECT(!entry->javascript_bytecode_cache_vary_key.has_value());
cache->update_javascript_bytecode_cache(url, "GET"sv, *request_headers, 0, bytecode);
entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default);
VERIFY(entry.has_value());
VERIFY(entry->javascript_bytecode_cache.has_value());
EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes());
EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional<u64> { 0 });
}

View file

@ -8,6 +8,7 @@ set(TEST_SOURCES
TestCSSSyntaxParser.cpp
TestCSSTokenizer.cpp
TestCSSTokenStream.cpp
TestFetchResponse.cpp
TestFetchURL.cpp
TestHTMLTokenizer.cpp
TestMicrosyntax.cpp
@ -27,6 +28,7 @@ ladybird_utility(css-tokenizer SOURCES css-tokenizer.cpp LIBS LibFileSystem LibM
target_link_libraries(TestContentBlocker PRIVATE LibURL)
target_link_libraries(TestControlMessageQueue PRIVATE LibSync)
target_link_libraries(TestFetchResponse PRIVATE LibGC LibHTTP LibJS)
target_link_libraries(TestFetchURL PRIVATE LibURL)
target_link_libraries(TestSecureContexts PRIVATE LibURL)
target_link_libraries(TestSourceHighlighter PRIVATE LibURL LibWebView)

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibHTTP/HeaderList.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/VM.h>
#include <LibTest/TestCase.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
TEST_CASE(javascript_bytecode_cache_memory_cache_request_headers_are_cloned)
{
auto vm = JS::VM::create();
auto root_execution_context = JS::create_simple_execution_context<JS::GlobalObject>(*vm);
auto& realm = *root_execution_context->realm;
auto request_headers = HTTP::HeaderList::create({
{ "User-Agent"sv, "Ladybird"sv },
});
auto response = Web::Fetch::Infrastructure::Response::create(*vm);
response->set_javascript_bytecode_cache_memory_cache_request_headers(request_headers);
auto cloned_response = response->clone(realm);
auto const& cloned_request_headers = cloned_response->javascript_bytecode_cache_memory_cache_request_headers();
request_headers->set({ "User-Agent"sv, "Changed"sv });
VERIFY(cloned_request_headers.has_value());
EXPECT_EQ((*cloned_request_headers)->get("User-Agent"sv), Optional<ByteString> { "Ladybird" });
}