tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mdom/script/ScriptLoader.cpp | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mjs/loader/LoadedScript.cpp | 4++++
Mjs/loader/LoadedScript.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/loader/ScriptLoadRequest.cpp | 15+++++++++++++++
Mjs/loader/ScriptLoadRequest.h | 16++++++++++++++++
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,