diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 09b211f2cf7d..60a2da676c4f 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -153,6 +153,7 @@ #include "nsHTMLTags.h" #include "nsIAnonymousContentCreator.h" #include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICacheInfoChannel.h" #include "nsICategoryManager.h" #include "nsIChannelEventSink.h" #include "nsIConsoleService.h" @@ -10357,3 +10358,37 @@ ScreenIntMargin nsContentUtils::GetWindowSafeAreaInsets( return windowSafeAreaInsets; } + +/* static */ +nsContentUtils::SubresourceCacheValidationInfo +nsContentUtils::GetSubresourceCacheValidationInfo(nsIRequest* aRequest) { + SubresourceCacheValidationInfo info; + if (nsCOMPtr cache = do_QueryInterface(aRequest)) { + uint32_t value = 0; + if (NS_SUCCEEDED(cache->GetCacheTokenExpirationTime(&value))) { + info.mExpirationTime.emplace(value); + } + } + + // Determine whether the cache entry must be revalidated when we try to use + // it. Currently, only HTTP specifies this information... + if (nsCOMPtr httpChannel = do_QueryInterface(aRequest)) { + Unused << httpChannel->IsNoStoreResponse(&info.mMustRevalidate); + + if (!info.mMustRevalidate) { + Unused << httpChannel->IsNoCacheResponse(&info.mMustRevalidate); + } + + // FIXME(bug 1644173): Why this check? + if (!info.mMustRevalidate) { + nsAutoCString cacheHeader; + Unused << httpChannel->GetResponseHeader( + NS_LITERAL_CSTRING("Cache-Control"), cacheHeader); + if (PL_strcasestr(cacheHeader.get(), "must-revalidate")) { + info.mMustRevalidate = true; + } + } + } + + return info; +} diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 3fe23fd94568..8f5890dada2b 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -48,6 +48,7 @@ #include "mozilla/dom/Document.h" #include "nsPIDOMWindow.h" #include "nsRFPService.h" +#include "prtime.h" #if defined(XP_WIN) // Undefine LoadImage to prevent naming conflict with Windows. @@ -3246,6 +3247,23 @@ class nsContentUtils { nsIScreen* aScreen, const mozilla::ScreenIntMargin& aSafeareaInsets, const mozilla::LayoutDeviceIntRect& aWindowRect); + struct SubresourceCacheValidationInfo { + // The expiration time, in seconds, if known. + Maybe mExpirationTime; + bool mMustRevalidate = false; + }; + + /** + * Gets cache validation info for subresources such as images or CSS + * stylesheets. + */ + static SubresourceCacheValidationInfo GetSubresourceCacheValidationInfo( + nsIRequest*); + + static uint32_t SecondsFromPRTime(PRTime aTime) { + return uint32_t(int64_t(aTime) / int64_t(PR_USEC_PER_SEC)); + } + private: static bool InitializeEventTable(); diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp index 6178d8cbde13..fa9046f4b6f9 100644 --- a/image/imgLoader.cpp +++ b/image/imgLoader.cpp @@ -960,11 +960,11 @@ static nsresult NewImageChannel( return NS_OK; } -/* static */ -uint32_t imgCacheEntry::SecondsFromPRTime(PRTime prTime) { - return uint32_t(int64_t(prTime) / int64_t(PR_USEC_PER_SEC)); +static uint32_t SecondsFromPRTime(PRTime aTime) { + return nsContentUtils::SecondsFromPRTime(aTime); } +/* static */ imgCacheEntry::imgCacheEntry(imgLoader* loader, imgRequest* request, bool forcePrincipalCheck) : mLoader(loader), @@ -1822,8 +1822,8 @@ bool imgLoader::ValidateEntry( // If the expiration time is zero, then the request has not gotten far enough // to know when it will expire. uint32_t expiryTime = aEntry->GetExpiryTime(); - bool hasExpired = expiryTime != 0 && - expiryTime <= imgCacheEntry::SecondsFromPRTime(PR_Now()); + bool hasExpired = + expiryTime != 0 && expiryTime <= SecondsFromPRTime(PR_Now()); nsresult rv; @@ -1840,8 +1840,7 @@ bool imgLoader::ValidateEntry( if (NS_SUCCEEDED(rv)) { // nsIFile uses millisec, NSPR usec fileLastMod *= 1000; - hasExpired = - imgCacheEntry::SecondsFromPRTime((PRTime)fileLastMod) > lastModTime; + hasExpired = SecondsFromPRTime((PRTime)fileLastMod) > lastModTime; } } } diff --git a/image/imgLoader.h b/image/imgLoader.h index 281f79c9158a..28ff373e4c35 100644 --- a/image/imgLoader.h +++ b/image/imgLoader.h @@ -39,8 +39,6 @@ namespace image {} // namespace image class imgCacheEntry { public: - static uint32_t SecondsFromPRTime(PRTime prTime); - imgCacheEntry(imgLoader* loader, imgRequest* request, bool aForcePrincipalCheck); ~imgCacheEntry(); diff --git a/image/imgRequest.cpp b/image/imgRequest.cpp index b71977df917a..bcb9803b245a 100644 --- a/image/imgRequest.cpp +++ b/image/imgRequest.cpp @@ -517,53 +517,26 @@ void imgRequest::UpdateCacheEntrySize() { void imgRequest::SetCacheValidation(imgCacheEntry* aCacheEntry, nsIRequest* aRequest) { /* get the expires info */ - if (aCacheEntry) { - // Expiration time defaults to 0. We set the expiration time on our - // entry if it hasn't been set yet. - if (aCacheEntry->GetExpiryTime() == 0) { - uint32_t expiration = 0; - nsCOMPtr cacheChannel(do_QueryInterface(aRequest)); - if (cacheChannel) { - /* get the expiration time from the caching channel's token */ - cacheChannel->GetCacheTokenExpirationTime(&expiration); - } - if (expiration == 0) { - // If the channel doesn't support caching, then ensure this expires the - // next time it is used. - expiration = imgCacheEntry::SecondsFromPRTime(PR_Now()) - 1; - } - aCacheEntry->SetExpiryTime(expiration); - } + if (!aCacheEntry || aCacheEntry->GetExpiryTime() != 0) { + return; + } - // Determine whether the cache entry must be revalidated when we try to use - // it. Currently, only HTTP specifies this information... - nsCOMPtr httpChannel(do_QueryInterface(aRequest)); - if (httpChannel) { - bool bMustRevalidate = false; + auto info = nsContentUtils::GetSubresourceCacheValidationInfo(aRequest); - Unused << httpChannel->IsNoStoreResponse(&bMustRevalidate); - - if (!bMustRevalidate) { - Unused << httpChannel->IsNoCacheResponse(&bMustRevalidate); - } - - if (!bMustRevalidate) { - nsAutoCString cacheHeader; - - Unused << httpChannel->GetResponseHeader( - NS_LITERAL_CSTRING("Cache-Control"), cacheHeader); - if (PL_strcasestr(cacheHeader.get(), "must-revalidate")) { - bMustRevalidate = true; - } - } - - // Cache entries default to not needing to validate. We ensure that - // multiple calls to this function don't override an earlier decision to - // validate by making validation a one-way decision. - if (bMustRevalidate) { - aCacheEntry->SetMustValidate(bMustRevalidate); - } - } + // Expiration time defaults to 0. We set the expiration time on our entry if + // it hasn't been set yet. + if (!info.mExpirationTime) { + // If the channel doesn't support caching, then ensure this expires the + // next time it is used. + info.mExpirationTime.emplace(nsContentUtils::SecondsFromPRTime(PR_Now()) - + 1); + } + aCacheEntry->SetExpiryTime(*info.mExpirationTime); + // Cache entries default to not needing to validate. We ensure that + // multiple calls to this function don't override an earlier decision to + // validate by making validation a one-way decision. + if (info.mMustRevalidate) { + aCacheEntry->SetMustValidate(info.mMustRevalidate); } } diff --git a/layout/style/Loader.cpp b/layout/style/Loader.cpp index e020bd140d99..e319b62eb5dd 100644 --- a/layout/style/Loader.cpp +++ b/layout/style/Loader.cpp @@ -22,6 +22,7 @@ #include "mozilla/URLPreloader.h" #include "nsIRunnable.h" #include "nsITimedChannel.h" +#include "nsICachingChannel.h" #include "nsSyncLoadService.h" #include "nsCOMPtr.h" #include "nsString.h" @@ -900,7 +901,7 @@ std::tuple, Loader::SheetState> Loader::CreateSheet( GetFallbackEncoding(*this, aLinkingContent, aPreloadOrParentDataEncoding), aCORSMode, aParsingMode, mCompatMode, sriMetadata, aIsPreload); - auto cacheResult = mSheets->Lookup(key, aSyncLoad); + auto cacheResult = mSheets->Lookup(*this, key, aSyncLoad); if (const auto& [styleSheet, sheetState] = cacheResult; styleSheet) { LOG((" Hit cache with state: %s", gStateStrings[size_t(sheetState)])); return cacheResult; @@ -1410,6 +1411,9 @@ Loader::Completed Loader::ParseSheet(const nsACString& aBytes, SheetLoadData& aLoadData, AllowAsyncParse aAllowAsync) { LOG(("css::Loader::ParseSheet")); + if (aLoadData.mURI) { + LOG_URI(" Load succeeded for URI: '%s', parsing", aLoadData.mURI); + } AUTO_PROFILER_LABEL("css::Loader::ParseSheet", LAYOUT_CSSParsing); ++mParsedSheetCount; @@ -2143,6 +2147,22 @@ nsIPrincipal* Loader::LoaderPrincipal() const { return nsContentUtils::GetSystemPrincipal(); } +bool Loader::ShouldBypassCache() const { + if (!mDocument) { + return false; + } + RefPtr lg = mDocument->GetDocumentLoadGroup(); + if (!lg) { + return false; + } + nsLoadFlags flags; + if (NS_FAILED(lg->GetLoadFlags(&flags))) { + return false; + } + return flags & (nsIRequest::LOAD_BYPASS_CACHE | + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE); +} + void Loader::BlockOnload() { if (mDocument) { mDocument->BlockOnload(); diff --git a/layout/style/Loader.h b/layout/style/Loader.h index 7673908f2842..da46d2c7d167 100644 --- a/layout/style/Loader.h +++ b/layout/style/Loader.h @@ -505,6 +505,8 @@ class Loader final { // owned by a document, or the system principal otherwise. nsIPrincipal* LoaderPrincipal() const; + bool ShouldBypassCache() const; + private: friend class mozilla::SharedStyleSheetCache; friend class SheetLoadData; diff --git a/layout/style/SharedStyleSheetCache.cpp b/layout/style/SharedStyleSheetCache.cpp index edcc5d131b0f..87f4fb4fd6f3 100644 --- a/layout/style/SharedStyleSheetCache.cpp +++ b/layout/style/SharedStyleSheetCache.cpp @@ -79,9 +79,16 @@ static void AssertIncompleteSheetMatches(const SheetLoadData& aData, "CSSOM shouldn't allow access to incomplete sheets"); } -auto SharedStyleSheetCache::Lookup(SheetLoadDataHashKey& aKey, bool aSyncLoad) - -> CacheResult { +bool SharedStyleSheetCache::CompleteSheet::Expired() const { + return mExpirationTime && + mExpirationTime <= nsContentUtils::SecondsFromPRTime(PR_Now()); +} + +SharedStyleSheetCache::CacheResult SharedStyleSheetCache::Lookup( + css::Loader& aLoader, const SheetLoadDataHashKey& aKey, bool aSyncLoad) { nsIURI* uri = aKey.URI(); + LOG(("SharedStyleSheetCache::Lookup(%s)", uri->GetSpecOrDefault().get())); + // Try to find first in the XUL prototype cache. if (dom::IsChromeURI(uri)) { nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); @@ -100,28 +107,35 @@ auto SharedStyleSheetCache::Lookup(SheetLoadDataHashKey& aKey, bool aSyncLoad) return {CloneSheet(*sheet), SheetState::Complete}; } - LOG( - (" Not cloning due to forced unique inner or mismatched " - "parsing mode")); + LOG((" Not cloning due to mismatched parsing mode")); } } } // Now complete sheets. if (auto lookup = mCompleteSheets.Lookup(aKey)) { - LOG((" From completed: %p", lookup.Data().get())); - AssertComplete(*lookup.Data()); - MOZ_ASSERT(lookup.Data()->ParsingMode() == aKey.ParsingMode()); + const CompleteSheet& completeSheet = lookup.Data(); // We can assert the stylesheet has not been modified, as we clone it on // insertion. - StyleSheet* cachedSheet = lookup.Data(); - MOZ_ASSERT(!cachedSheet->HasForcedUniqueInner()); - MOZ_ASSERT(!cachedSheet->HasModifiedRules()); + StyleSheet& cachedSheet = *completeSheet.mSheet; + LOG((" From completed: %p", &cachedSheet)); - RefPtr clone = CloneSheet(*cachedSheet); - MOZ_ASSERT(!clone->HasForcedUniqueInner()); - MOZ_ASSERT(!clone->HasModifiedRules()); - return {std::move(clone), SheetState::Complete}; + if ((!aLoader.ShouldBypassCache() && !completeSheet.Expired()) || + aLoader.mLoadsPerformed.Contains(aKey)) { + LOG( + (" Not expired yet, or previously loaded already in " + "that document")); + + AssertComplete(cachedSheet); + MOZ_ASSERT(cachedSheet.ParsingMode() == aKey.ParsingMode()); + MOZ_ASSERT(!cachedSheet.HasForcedUniqueInner()); + MOZ_ASSERT(!cachedSheet.HasModifiedRules()); + + RefPtr clone = CloneSheet(cachedSheet); + MOZ_ASSERT(!clone->HasForcedUniqueInner()); + MOZ_ASSERT(!clone->HasModifiedRules()); + return {std::move(clone), SheetState::Complete}; + } } if (aSyncLoad) { @@ -189,7 +203,7 @@ size_t SharedStyleSheetCache::SizeOfIncludingThis( n += mCompleteSheets.ShallowSizeOfExcludingThis(aMallocSizeOf); for (auto iter = mCompleteSheets.ConstIter(); !iter.Done(); iter.Next()) { - n += iter.UserData()->SizeOfIncludingThis(aMallocSizeOf); + n += iter.UserData().mSheet->SizeOfIncludingThis(aMallocSizeOf); } // Measurement of the following members may be added later if DMD finds it is @@ -312,19 +326,30 @@ void SharedStyleSheetCache::InsertIntoCompleteCacheIfNeeded( SheetLoadData& aData) { MOZ_ASSERT(aData.mLoader->GetDocument(), "We only cache document-associated sheets"); + LOG(("SharedStyleSheetCache::InsertIntoCompleteCacheIfNeeded")); // If we ever start doing this for failed loads, we'll need to adjust the // PostLoadEvent code that thinks anything already complete must have loaded // succesfully. if (aData.mLoadFailed) { + LOG((" Load failed, bailing")); + return; + } + + // If this sheet came from the cache already, there's no need to override + // anything. + if (aData.mSheetAlreadyComplete) { + LOG((" Sheet came from the cache, bailing")); return; } if (!aData.mURI) { + LOG((" Inline style sheet, bailing")); // Inline sheet caching happens in Loader::mInlineSheets. return; } if (aData.mSheet->IsConstructed()) { + LOG((" Constructable style sheet, bailing")); // Constructable sheets are not worth caching, they're always unique. return; } @@ -352,18 +377,25 @@ void SharedStyleSheetCache::InsertIntoCompleteCacheIfNeeded( } } } else { + LOG((" Putting style sheet in shared cache: %s", + aData.mURI->GetSpecOrDefault().get())); SheetLoadDataHashKey key(aData); MOZ_ASSERT(sheet->IsComplete(), "Should only be caching complete sheets"); #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED for (const auto& entry : mCompleteSheets) { - MOZ_DIAGNOSTIC_ASSERT( - entry.GetData() != sheet || key.KeyEquals(entry.GetKey()), - "Same sheet, different keys?"); + if (!key.KeyEquals(entry.GetKey())) { + MOZ_DIAGNOSTIC_ASSERT(entry.GetData().mSheet != sheet, + "Same sheet, different keys?"); + } else { + MOZ_DIAGNOSTIC_ASSERT( + entry.GetData().Expired() || aData.mLoader->ShouldBypassCache(), + "Overriding existing complete entry?"); + } } #endif - mCompleteSheets.Put(key, std::move(sheet)); + mCompleteSheets.Put(key, {aData.mExpirationTime, std::move(sheet)}); } } diff --git a/layout/style/SharedStyleSheetCache.h b/layout/style/SharedStyleSheetCache.h index 734086ab091b..22311d970880 100644 --- a/layout/style/SharedStyleSheetCache.h +++ b/layout/style/SharedStyleSheetCache.h @@ -55,7 +55,7 @@ class SharedStyleSheetCache final : public nsIMemoryReporter { // A cache hit or miss. It is a miss if the `StyleSheet` is null. using CacheResult = std::tuple, css::Loader::SheetState>; - CacheResult Lookup(SheetLoadDataHashKey&, bool aSyncLoad); + CacheResult Lookup(css::Loader&, const SheetLoadDataHashKey&, bool aSyncLoad); // Tries to coalesce with an already existing load. The sheet state must be // the one that Lookup returned, if it returned a sheet. @@ -112,7 +112,14 @@ class SharedStyleSheetCache final : public nsIMemoryReporter { ~SharedStyleSheetCache(); - nsRefPtrHashtable mCompleteSheets; + struct CompleteSheet { + uint32_t mExpirationTime = 0; + RefPtr mSheet; + + bool Expired() const; + }; + + nsDataHashtable mCompleteSheets; nsRefPtrHashtable mPendingDatas; // The SheetLoadData pointers in mLoadingDatas below are weak references. // diff --git a/layout/style/SheetLoadData.h b/layout/style/SheetLoadData.h index 1d7af0b0d654..040245ef38ad 100644 --- a/layout/style/SheetLoadData.h +++ b/layout/style/SheetLoadData.h @@ -111,6 +111,10 @@ class SheetLoadData final : public nsIRunnable, public nsIThreadObserver { // during the parse const RefPtr mParentData; + // The expiration time of the channel that has loaded this data, if + // applicable. + uint32_t mExpirationTime = 0; + // Number of sheets we @import-ed that are still loading uint32_t mPendingChildren; diff --git a/layout/style/StreamLoader.cpp b/layout/style/StreamLoader.cpp index 04b133322ac8..561bbca0fd81 100644 --- a/layout/style/StreamLoader.cpp +++ b/layout/style/StreamLoader.cpp @@ -8,6 +8,7 @@ #include "mozilla/Encoding.h" #include "mozilla/ScopeExit.h" +#include "nsContentUtils.h" #include "nsIChannel.h" #include "nsIInputStream.h" #include "nsISupportsPriority.h" @@ -132,6 +133,15 @@ StreamLoader::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { } } // run destructor for `bytes` + auto info = nsContentUtils::GetSubresourceCacheValidationInfo(aRequest); + + // For now, we never cache entries that we have to revalidate. + if (!info.mExpirationTime || info.mMustRevalidate) { + info.mExpirationTime = + Some(nsContentUtils::SecondsFromPRTime(PR_Now()) - 1); + } + mSheetLoadData->mExpirationTime = *info.mExpirationTime; + // For reasons I don't understand, factoring the below lines into // a method on SheetLoadData resulted in a linker error. Hence, // accessing fields of mSheetLoadData from here.