commit ae2826ed85991770191016c90b8ca8c110a928db
parent ea935a9c73b707bc76a5fc352d5389e6dddb990a
Author: Tooru Fujisawa <arai_a@mac.com>
Date: Wed, 19 Nov 2025 00:44:39 +0000
Bug 1998925 - Part 1: Add dirty flag to LoadedScript. r=nbp
Differential Revision: https://phabricator.services.mozilla.com/D271875
Diffstat:
5 files changed, 154 insertions(+), 8 deletions(-)
diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp
@@ -1195,6 +1195,20 @@ void ScriptLoader::TryUseCache(ReferrerPolicy aReferrerPolicy,
return;
}
+ if (cacheResult.mCompleteValue->IsDirty()) {
+ // The cache entry needs revalidation.
+ // Fetch from necko and validate in ScriptLoader::OnStreamComplete.
+ TRACE_FOR_TEST(aRequest, "memorycache:dirty:hit");
+ aRequest->SetHasDirtyCache();
+ aRequest->NoCacheEntryFound(aReferrerPolicy, aFetchOptions, aURI);
+ LOG(
+ ("ScriptLoader (%p): Created LoadedScript (%p) for "
+ "ScriptLoadRequest(%p) because of dirty flag %s.",
+ this, aRequest->getLoadedScript(), aRequest,
+ aRequest->URI()->GetSpecOrDefault().get()));
+ return;
+ }
+
if (aRequestType == ScriptLoadRequestType::External) {
// NOTE: The preload case checks the same after the
// LookupPreloadRequest call.
@@ -1224,9 +1238,7 @@ void ScriptLoader::TryUseCache(ReferrerPolicy aReferrerPolicy,
aRequest->URI()->GetSpecOrDefault().get()));
TRACE_FOR_TEST(aRequest, "load:memorycache");
- if (cacheResult.mCompleteValue->mFetchCount < UINT8_MAX) {
- cacheResult.mCompleteValue->mFetchCount++;
- }
+ cacheResult.mCompleteValue->AddFetchCount();
return;
}
@@ -2025,6 +2037,11 @@ nsresult ScriptLoader::AttemptOffThreadScriptCompile(
return NS_OK;
}
+ if (aRequest->IsCachedStencil()) {
+ // This is a revived cache.
+ return NS_OK;
+ }
+
// Don't off-thread compile JSON or CSS modules.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1912112 (JSON)
// https://bugzilla.mozilla.org/show_bug.cgi?id=1987143 (CSS)
@@ -4072,16 +4089,56 @@ nsresult ScriptLoader::OnStreamComplete(
nsresult rv = VerifySRI(aRequest, aLoader, aSRIStatus, aSRIDataVerifier);
if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIRequest> channelRequest;
+ aLoader->GetRequest(getter_AddRefs(channelRequest));
+
+ nsCOMPtr<nsICacheInfoChannel> cacheInfo = do_QueryInterface(channelRequest);
+
+ if (cacheInfo) {
+ uint64_t id;
+ nsresult rv = cacheInfo->GetCacheEntryId(&id);
+ if (NS_SUCCEEDED(rv)) {
+ LOG(("ScriptLoadRequest (%p): cacheEntryId = %zx", aRequest,
+ size_t(id)));
+
+ if (aRequest->HasDirtyCache()) {
+ // This request found a dirty cache.
+ // Validate the cache with the response's cache ID.
+ ScriptHashKey key(this, aRequest, aRequest->FetchOptions(),
+ aRequest->URI());
+ auto cacheResult = mCache->Lookup(*this, key, /* aSyncLoad = */ true);
+ if (cacheResult.mState == CachedSubResourceState::Complete &&
+ cacheResult.mCompleteValue->CacheEntryId() == id) {
+ cacheResult.mCompleteValue->UnsetDirty();
+ // This keeps the request as "fetching" state.
+ // PrepareLoadedRequest below will set it to "ready" state.
+ //
+ // Off-thread compilation is skipped for the revived cache.
+ // See AttemptOffThreadScriptCompile.
+ //
+ // Main thread compilation is skipped in the same way as
+ // non-dirty cache.
+ aRequest->CacheEntryRevived(cacheResult.mCompleteValue);
+
+ cacheResult.mCompleteValue->AddFetchCount();
+
+ TRACE_FOR_TEST(aRequest, "memorycache:dirty:revived");
+ } else {
+ mCache->Evict(key);
+ TRACE_FOR_TEST(aRequest, "memorycache:dirty:evicted");
+ }
+ }
+
+ aRequest->getLoadedScript()->SetCacheEntryId(id);
+ }
+ }
+
// If we are loading from source, store the cache info channel and
// save the computed SRI hash or a dummy SRI hash in case we are going to
// save the bytecode of this script in the cache.
if (aRequest->IsSource() &&
StaticPrefs::dom_script_loader_bytecode_cache_enabled()) {
- nsCOMPtr<nsIRequest> channelRequest;
- aLoader->GetRequest(getter_AddRefs(channelRequest));
-
- aRequest->getLoadedScript()->mCacheInfo =
- do_QueryInterface(channelRequest);
+ aRequest->getLoadedScript()->mCacheInfo = cacheInfo;
LOG(("ScriptLoadRequest (%p): nsICacheInfoChannel = %p", aRequest,
aRequest->getLoadedScript()->mCacheInfo.get()));
diff --git a/js/loader/LoadedScript.cpp b/js/loader/LoadedScript.cpp
@@ -53,6 +53,8 @@ LoadedScript::LoadedScript(ScriptKind aKind,
mKind(aKind),
mReferrerPolicy(aReferrerPolicy),
mBytecodeOffset(0),
+ mCacheEntryId(InvalidCacheEntryId),
+ mIsDirty(false),
mFetchOptions(aFetchOptions),
mURI(aURI),
mReceivedScriptTextLength(0) {
@@ -65,6 +67,8 @@ LoadedScript::LoadedScript(const LoadedScript& aOther)
mKind(aOther.mKind),
mReferrerPolicy(aOther.mReferrerPolicy),
mBytecodeOffset(0),
+ mCacheEntryId(aOther.mCacheEntryId),
+ mIsDirty(aOther.mIsDirty),
mFetchOptions(aOther.mFetchOptions),
mURI(aOther.mURI),
mBaseURL(aOther.mBaseURL),
diff --git a/js/loader/LoadedScript.h b/js/loader/LoadedScript.h
@@ -299,6 +299,34 @@ class LoadedScript : public nsIMemoryReporter {
void SetBaseURLFromChannelAndOriginalURI(nsIChannel* aChannel,
nsIURI* aOriginalURI);
+ bool IsDirty() const { return mIsDirty; }
+ void SetDirty() {
+ MOZ_ASSERT(HasCacheEntryId());
+ mIsDirty = true;
+ }
+ void UnsetDirty() {
+ MOZ_ASSERT(HasCacheEntryId());
+ mIsDirty = false;
+ }
+
+ bool HasCacheEntryId() const { return mCacheEntryId != InvalidCacheEntryId; }
+ uint64_t CacheEntryId() const {
+ MOZ_ASSERT(HasCacheEntryId());
+ return mCacheEntryId;
+ }
+ void SetCacheEntryId(uint64_t aId) {
+ mCacheEntryId = aId;
+
+ // mCacheEntryId is 48bits. Verify no overflow happened.
+ MOZ_ASSERT(mCacheEntryId == aId);
+ }
+
+ void AddFetchCount() {
+ if (mFetchCount < UINT8_MAX) {
+ mFetchCount++;
+ }
+ }
+
public:
// Fields.
@@ -325,6 +353,32 @@ class LoadedScript : public nsIMemoryReporter {
uint32_t mBytecodeOffset;
private:
+ static constexpr uint64_t InvalidCacheEntryId = 0;
+
+ // The cache entry ID of this script.
+ //
+ // 0 if the response doesn't have the corresponding cache entry,
+ // or any other failure happened.
+ //
+ // This value comes from mozilla::net::CacheEntry::mCacheEntryId,
+ // which comes from mozilla::net::CacheEntry::GetNextId.
+ // It generates sequential IDs from 1 (thus 0 is treated as invalid value),
+ // and the ID is valid within single browser session.
+ //
+ // In order to pack this field with mIsDirty below, we use shorter bits than
+ // the original mozilla::net::CacheEntry::mCacheEntryId type (uint64_t).
+ //
+ // As long as the per-session sequential ID is the sole source of this value,
+ // 48 bits should be sufficient. 1000 new IDs per second for 365 days
+ // becomes 0x7_57b1_2c00, which is 35 bits.
+ uint64_t mCacheEntryId : 48;
+
+ // Set to true in the following situation:
+ // * this is cached in SharedScriptCache
+ // * A behavior around the network request is modified, and
+ // the cache needs validation on the necko side
+ bool mIsDirty : 1;
+
RefPtr<ScriptFetchOptions> mFetchOptions;
nsCOMPtr<nsIURI> mURI;
diff --git a/js/loader/ScriptLoadRequest.cpp b/js/loader/ScriptLoadRequest.cpp
@@ -93,6 +93,7 @@ ScriptLoadRequest::ScriptLoadRequest(ScriptKind aKind,
mState(State::CheckingCache),
mFetchSourceOnly(false),
mHasSourceMapURL_(false),
+ mHasDirtyCache_(false),
mDiskCachingPlan(CachingPlan::Uninitialized),
mMemoryCachingPlan(CachingPlan::Uninitialized),
mIntegrity(aIntegrity),
@@ -165,6 +166,20 @@ const ModuleLoadRequest* ScriptLoadRequest::AsModuleRequest() const {
void ScriptLoadRequest::CacheEntryFound(LoadedScript* aLoadedScript) {
MOZ_ASSERT(IsCheckingCache());
+ SetCacheEntry(aLoadedScript);
+}
+
+void ScriptLoadRequest::CacheEntryRevived(LoadedScript* aLoadedScript) {
+ MOZ_ASSERT(IsFetching());
+
+ SetCacheEntry(aLoadedScript);
+
+ // NOTE: The caller should keep using the "fetching" path, with the
+ // cached stencil, and skip the compilation.
+ mState = State::Fetching;
+}
+
+void ScriptLoadRequest::SetCacheEntry(LoadedScript* aLoadedScript) {
switch (mKind) {
case ScriptKind::eClassic:
MOZ_ASSERT(aLoadedScript->IsClassicScript());
diff --git a/js/loader/ScriptLoadRequest.h b/js/loader/ScriptLoadRequest.h
@@ -168,12 +168,18 @@ class ScriptLoadRequest : public nsISupports,
// the script data from cached script.
void CacheEntryFound(LoadedScript* aLoadedScript);
+ void CacheEntryRevived(LoadedScript* aLoadedScript);
+
// Convert a CheckingCache ScriptLoadRequest into a Fetching one, by creating
// a new LoadedScript which is matching the ScriptKind provided when
// constructing this ScriptLoadRequest.
void NoCacheEntryFound(mozilla::dom::ReferrerPolicy aReferrerPolicy,
ScriptFetchOptions* aFetchOptions, nsIURI* aURI);
+ private:
+ void SetCacheEntry(LoadedScript* aLoadedScript);
+
+ public:
bool PassedConditionForDiskCache() const {
return mDiskCachingPlan == CachingPlan::PassedCondition;
}
@@ -255,6 +261,9 @@ class ScriptLoadRequest : public nsISupports,
mHasSourceMapURL_ = true;
}
+ bool HasDirtyCache() const { return mHasDirtyCache_; }
+ void SetHasDirtyCache() { mHasDirtyCache_ = true; }
+
public:
// Fields.
@@ -273,6 +282,13 @@ class ScriptLoadRequest : public nsISupports,
// Use HasSourceMapURL(), SetSourceMapURL(), and GetSourceMapURL().
bool mHasSourceMapURL_ : 1;
+ // Set to true if this response is found in the in-memory cache, but the
+ // cache is marked as dirty, and needs validation.
+ //
+ // This request should go to necko, and when the response is received,
+ // the cache should be either revived or evicted.
+ bool mHasDirtyCache_ : 1;
+
enum class CachingPlan : uint8_t {
// This is not yet considered for caching.
Uninitialized,