Bug 1599160 - Better integration of the shared stylesheet cache with the network cache. r=tnikkel,mayhemer,heycam

Make the stylesheet cache respect the same headers as the image cache
does. This makes no-cache stylesheets work as they do now, which is
useful for developers that want to develop sites locally, and for
shift-reloads, etc.

Differential Revision: https://phabricator.services.mozilla.com/D78659
This commit is contained in:
Emilio Cobos Álvarez 2020-06-11 11:42:01 +00:00
parent d999791a43
commit 5048e0ec84
11 changed files with 175 additions and 77 deletions

View File

@ -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<nsICacheInfoChannel> 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<nsIHttpChannel> 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;
}

View File

@ -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<uint32_t> 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();

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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<nsICacheInfoChannel> 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<nsIHttpChannel> 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);
}
}

View File

@ -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<RefPtr<StyleSheet>, 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<nsILoadGroup> 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();

View File

@ -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;

View File

@ -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<StyleSheet> 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<StyleSheet> 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)});
}
}

View File

@ -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<RefPtr<StyleSheet>, 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<SheetLoadDataHashKey, StyleSheet> mCompleteSheets;
struct CompleteSheet {
uint32_t mExpirationTime = 0;
RefPtr<StyleSheet> mSheet;
bool Expired() const;
};
nsDataHashtable<SheetLoadDataHashKey, CompleteSheet> mCompleteSheets;
nsRefPtrHashtable<SheetLoadDataHashKey, css::SheetLoadData> mPendingDatas;
// The SheetLoadData pointers in mLoadingDatas below are weak references.
//

View File

@ -111,6 +111,10 @@ class SheetLoadData final : public nsIRunnable, public nsIThreadObserver {
// during the parse
const RefPtr<SheetLoadData> 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;

View File

@ -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.