gecko-dev/layout/style/SharedStyleSheetCache.cpp
Emilio Cobos Álvarez 60812fc63c Bug 1711437 - Don't EnsureUniqueInner from the cssRules getter. r=layout-reviewers,jfkthame
Instead, fix up the various content data structures when the stylesheet
is mutated. This makes reading a stylesheet not disable style sharing.

Differential Revision: https://phabricator.services.mozilla.com/D115203
2021-07-20 13:17:02 +00:00

630 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SharedStyleSheetCache.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/StyleSheet.h"
#include "mozilla/css/SheetLoadData.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Document.h"
#include "mozilla/ServoBindings.h"
#include "nsContentUtils.h"
#include "nsXULPrototypeCache.h"
extern mozilla::LazyLogModule sCssLoaderLog;
#define LOG(args) MOZ_LOG(sCssLoaderLog, mozilla::LogLevel::Debug, args)
namespace mozilla {
using css::SheetLoadData;
using SheetState = css::Loader::SheetState;
using LoadDataArray = css::Loader::LoadDataArray;
using IsAlternate = css::Loader::IsAlternate;
SharedStyleSheetCache* SharedStyleSheetCache::sInstance;
void SharedStyleSheetCache::Clear(nsIPrincipal* aForPrincipal,
const nsACString* aBaseDomain) {
using ContentParent = dom::ContentParent;
if (XRE_IsParentProcess()) {
auto forPrincipal = aForPrincipal ? Some(RefPtr(aForPrincipal)) : Nothing();
auto baseDomain = aBaseDomain ? Some(nsCString(*aBaseDomain)) : Nothing();
for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) {
Unused << cp->SendClearStyleSheetCache(forPrincipal, baseDomain);
}
}
if (!sInstance) {
return;
}
// No filter, clear all.
if (!aForPrincipal && !aBaseDomain) {
sInstance->mCompleteSheets.Clear();
return;
}
for (auto iter = sInstance->mCompleteSheets.Iter(); !iter.Done();
iter.Next()) {
const bool shouldRemove = [&] {
if (aForPrincipal && iter.Key().Principal()->Equals(aForPrincipal)) {
return true;
}
if (!aBaseDomain) {
return false;
}
// Clear by baseDomain.
nsIPrincipal* partitionPrincipal = iter.Key().PartitionPrincipal();
// Clear entries with matching base domain. This includes entries
// which are partitioned under other top level sites (= have a
// partitionKey set).
nsAutoCString principalBaseDomain;
nsresult rv = partitionPrincipal->GetBaseDomain(principalBaseDomain);
if (NS_SUCCEEDED(rv) && principalBaseDomain.Equals(*aBaseDomain)) {
return true;
}
// Clear entries partitioned under aBaseDomain.
return StoragePrincipalHelper::PartitionKeyHasBaseDomain(
partitionPrincipal->OriginAttributesRef().mPartitionKey,
*aBaseDomain);
}();
if (shouldRemove) {
iter.Remove();
}
}
}
already_AddRefed<SharedStyleSheetCache> SharedStyleSheetCache::Create() {
MOZ_DIAGNOSTIC_ASSERT(!sInstance);
RefPtr<SharedStyleSheetCache> cache = new SharedStyleSheetCache();
sInstance = cache.get();
RegisterWeakMemoryReporter(cache.get());
return cache.forget();
}
SharedStyleSheetCache::~SharedStyleSheetCache() {
MOZ_DIAGNOSTIC_ASSERT(sInstance == this);
UnregisterWeakMemoryReporter(this);
sInstance = nullptr;
}
NS_IMPL_ISUPPORTS(SharedStyleSheetCache, nsIMemoryReporter)
MOZ_DEFINE_MALLOC_SIZE_OF(SharedStyleSheetCacheMallocSizeOf)
NS_IMETHODIMP
SharedStyleSheetCache::CollectReports(nsIHandleReportCallback* aHandleReport,
nsISupports* aData, bool aAnonymize) {
MOZ_COLLECT_REPORT("explicit/layout/style-sheet-cache/document-shared",
KIND_HEAP, UNITS_BYTES,
SizeOfIncludingThis(SharedStyleSheetCacheMallocSizeOf),
"Memory used for SharedStyleSheetCache to share style "
"sheets across documents (not to be confused with "
"GlobalStyleSheetCache)");
return NS_OK;
}
static RefPtr<StyleSheet> CloneSheet(StyleSheet& aSheet) {
return aSheet.Clone(nullptr, nullptr);
}
static void AssertComplete(const StyleSheet& aSheet) {
// This sheet came from the XUL cache or SharedStyleSheetCache; it better be a
// complete sheet.
MOZ_ASSERT(aSheet.IsComplete(),
"Sheet thinks it's not complete while we think it is");
}
static void AssertIncompleteSheetMatches(const SheetLoadData& aData,
const SheetLoadDataHashKey& aKey) {
MOZ_ASSERT(aKey.Principal()->Equals(aData.mTriggeringPrincipal),
"Principals should be the same");
MOZ_ASSERT(!aData.mSheet->HasForcedUniqueInner(),
"CSSOM shouldn't allow access to incomplete sheets");
}
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();
if (cache && cache->IsEnabled()) {
if (StyleSheet* sheet = cache->GetStyleSheet(uri)) {
LOG((" From XUL cache: %p", sheet));
AssertComplete(*sheet);
// See below, we always clone on insertion so we can guarantee the
// stylesheet is not modified.
MOZ_ASSERT(!sheet->HasForcedUniqueInner());
// We need to check the parsing mode manually because the XUL cache only
// keys off the URI. But we should fix that!
if (sheet->ParsingMode() == aKey.ParsingMode()) {
aLoader.DidHitCompleteSheetCache(aKey, nullptr);
return {CloneSheet(*sheet), SheetState::Complete};
}
LOG((" Not cloning due to mismatched parsing mode"));
}
}
}
// Now complete sheets.
if (auto lookup = mCompleteSheets.Lookup(aKey)) {
const CompleteSheet& completeSheet = lookup.Data();
// We can assert the stylesheet has not been modified, as we clone it on
// insertion.
StyleSheet& cachedSheet = *completeSheet.mSheet;
LOG((" From completed: %p, bypass: %d, expired: %d", &cachedSheet,
aLoader.ShouldBypassCache(), completeSheet.Expired()));
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());
aLoader.DidHitCompleteSheetCache(aKey, completeSheet.mUseCounters.get());
return {std::move(clone), SheetState::Complete};
}
}
if (aSyncLoad) {
return {};
}
if (SheetLoadData* data = mLoadingDatas.Get(aKey)) {
LOG((" From loading: %p", data->mSheet.get()));
AssertIncompleteSheetMatches(*data, aKey);
return {CloneSheet(*data->mSheet), SheetState::Loading};
}
if (SheetLoadData* data = mPendingDatas.GetWeak(aKey)) {
LOG((" From pending: %p", data->mSheet.get()));
AssertIncompleteSheetMatches(*data, aKey);
return {CloneSheet(*data->mSheet), SheetState::Pending};
}
return {};
}
void SharedStyleSheetCache::WillStartPendingLoad(SheetLoadData& aData) {
SheetLoadData* curr = &aData;
do {
MOZ_DIAGNOSTIC_ASSERT(curr->mLoader->mPendingLoadCount,
"Where did this pending load come from?");
--curr->mLoader->mPendingLoadCount;
} while ((curr = curr->mNext));
}
bool SharedStyleSheetCache::CoalesceLoad(const SheetLoadDataHashKey& aKey,
SheetLoadData& aNewLoad,
SheetState aExistingLoadState) {
MOZ_ASSERT(SheetLoadDataHashKey(aNewLoad).KeyEquals(aKey));
SheetLoadData* existingData = nullptr;
if (aExistingLoadState == SheetState::Loading) {
existingData = mLoadingDatas.Get(aKey);
MOZ_ASSERT(existingData, "CreateSheet lied about the state");
} else if (aExistingLoadState == SheetState::Pending) {
existingData = mPendingDatas.GetWeak(aKey);
MOZ_ASSERT(existingData, "CreateSheet lied about the state");
}
if (!existingData) {
return false;
}
if (aExistingLoadState == SheetState::Pending && !aNewLoad.ShouldDefer()) {
// Kick the load off; someone cares about it right away
RefPtr<SheetLoadData> removedData;
mPendingDatas.Remove(aKey, getter_AddRefs(removedData));
MOZ_ASSERT(removedData == existingData, "Bad loading table");
WillStartPendingLoad(*removedData);
// We insert to the front instead of the back, to keep the invariant that
// the front sheet always is the one that triggers the load.
aNewLoad.mNext = std::move(removedData);
LOG((" Forcing load of pending data"));
return false;
}
LOG((" Glomming on to existing load"));
SheetLoadData* data = existingData;
while (data->mNext) {
data = data->mNext;
}
data->mNext = &aNewLoad;
return true;
}
size_t SharedStyleSheetCache::SizeOfIncludingThis(
MallocSizeOf aMallocSizeOf) const {
size_t n = aMallocSizeOf(this);
n += mCompleteSheets.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (const auto& data : mCompleteSheets.Values()) {
n += data.mSheet->SizeOfIncludingThis(aMallocSizeOf);
n += aMallocSizeOf(data.mUseCounters.get());
}
// Measurement of the following members may be added later if DMD finds it is
// worthwhile:
// - mLoadingDatas: transient, and should be small
// - mPendingDatas: transient, and should be small
return n;
}
void SharedStyleSheetCache::DeferSheetLoad(const SheetLoadDataHashKey& aKey,
SheetLoadData& aData) {
MOZ_ASSERT(SheetLoadDataHashKey(aData).KeyEquals(aKey));
MOZ_DIAGNOSTIC_ASSERT(!aData.mNext, "Should only defer loads once");
aData.mMustNotify = true;
mPendingDatas.InsertOrUpdate(aKey, RefPtr{&aData});
}
void SharedStyleSheetCache::LoadStarted(const SheetLoadDataHashKey& aKey,
SheetLoadData& aData) {
MOZ_ASSERT(aData.mURI, "No load required?");
MOZ_ASSERT(!aData.mIsLoading, "Already loading? How?");
MOZ_ASSERT(SheetLoadDataHashKey(aData).KeyEquals(aKey));
aData.mIsLoading = true;
mLoadingDatas.InsertOrUpdate(aKey, &aData);
}
void SharedStyleSheetCache::LoadCompleted(SharedStyleSheetCache* aCache,
SheetLoadData& aData,
nsresult aStatus) {
// If aStatus is a failure we need to mark this data failed. We also need to
// mark any ancestors of a failing data as failed and any sibling of a
// failing data as failed. Note that SheetComplete is never called on a
// SheetLoadData that is the mNext of some other SheetLoadData.
nsresult cancelledStatus = aStatus;
if (NS_FAILED(aStatus)) {
css::Loader::MarkLoadTreeFailed(aData);
} else {
cancelledStatus = NS_BINDING_ABORTED;
SheetLoadData* data = &aData;
do {
if (data->mIsCancelled) {
// We only need to mark loads for this loader as cancelled, so as to not
// fire error events in unrelated documents.
css::Loader::MarkLoadTreeFailed(*data, data->mLoader);
}
} while ((data = data->mNext));
}
// 8 is probably big enough for all our common cases. It's not likely that
// imports will nest more than 8 deep, and multiple sheets with the same URI
// are rare.
AutoTArray<RefPtr<SheetLoadData>, 8> datasToNotify;
LoadCompletedInternal(aCache, aData, datasToNotify);
// Now it's safe to go ahead and notify observers
for (RefPtr<SheetLoadData>& data : datasToNotify) {
auto status = data->mIsCancelled ? cancelledStatus : aStatus;
data->mLoader->NotifyObservers(*data, status);
}
}
void SharedStyleSheetCache::LoadCompletedInternal(
SharedStyleSheetCache* aCache, SheetLoadData& aData,
nsTArray<RefPtr<SheetLoadData>>& aDatasToNotify) {
if (aData.mIsLoading) {
MOZ_ASSERT(aCache);
SheetLoadDataHashKey key(aData);
Maybe<SheetLoadData*> loadingData = aCache->mLoadingDatas.Extract(key);
MOZ_DIAGNOSTIC_ASSERT(loadingData);
MOZ_DIAGNOSTIC_ASSERT(loadingData.value() == &aData);
Unused << loadingData;
aData.mIsLoading = false;
}
// Go through and deal with the whole linked list.
SheetLoadData* data = &aData;
do {
MOZ_DIAGNOSTIC_ASSERT(!data->mSheetCompleteCalled);
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
data->mSheetCompleteCalled = true;
#endif
if (!data->mSheetAlreadyComplete) {
// If mSheetAlreadyComplete, then the sheet could well be modified between
// when we posted the async call to SheetComplete and now, since the sheet
// was page-accessible during that whole time.
// HasForcedUniqueInner() is okay if the sheet is constructed, because
// constructed sheets are always unique and they may be set to complete
// multiple times if their rules are replaced via Replace()
MOZ_ASSERT(data->mSheet->IsConstructed() ||
!data->mSheet->HasForcedUniqueInner(),
"should not get a forced unique inner during parsing");
// Insert the sheet into the tree now the sheet has loaded, but only if
// the sheet is still relevant, and if this is a top-level sheet.
const bool needInsertIntoTree = [&] {
if (!data->mLoader->GetDocument()) {
// Not a document load, nothing to do.
return false;
}
if (data->IsPreload()) {
// Preloads are not supposed to be observable.
return false;
}
if (data->mSheet->IsConstructed()) {
// Constructable sheets are not in the regular stylesheet tree.
return false;
}
if (data->mIsChildSheet) {
// A child sheet, those will get exposed from the parent, no need to
// insert them into the tree.
return false;
}
if (data->mOwningNode != data->mSheet->GetOwnerNode()) {
// The sheet was already removed from the tree and is no longer the
// current sheet of the owning node, we can bail.
return false;
}
return true;
}();
if (needInsertIntoTree) {
data->mLoader->InsertSheetInTree(*data->mSheet, data->mOwningNode);
}
data->mSheet->SetComplete();
data->ScheduleLoadEventIfNeeded();
} else if (data->mSheet->IsApplicable()) {
if (dom::Document* doc = data->mLoader->GetDocument()) {
// We post these events for devtools, even though the applicable state
// has not actually changed, to make the cache not observable.
doc->PostStyleSheetApplicableStateChangeEvent(*data->mSheet);
}
}
aDatasToNotify.AppendElement(data);
NS_ASSERTION(!data->mParentData || data->mParentData->mPendingChildren != 0,
"Broken pending child count on our parent");
// If we have a parent, our parent is no longer being parsed, and
// we are the last pending child, then our load completion
// completes the parent too. Note that the parent _can_ still be
// being parsed (eg if the child (us) failed to open the channel
// or some such).
if (data->mParentData && --(data->mParentData->mPendingChildren) == 0 &&
!data->mParentData->mIsBeingParsed) {
LoadCompletedInternal(aCache, *data->mParentData, aDatasToNotify);
}
data = data->mNext;
} while (data);
if (aCache) {
aCache->InsertIntoCompleteCacheIfNeeded(aData);
}
}
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 or constructable style sheet, bailing"));
// Inline sheet caching happens in Loader::mInlineSheets.
// Constructable sheets are not worth caching, they're always unique.
return;
}
// We need to clone the sheet on insertion to the cache because otherwise the
// stylesheets can keep alive full windows alive via either their JS wrapper,
// or via StyleSheet::mRelevantGlobal.
//
// If this ever changes, then you also need to fix up the memory reporting in
// both SizeOfIncludingThis and nsXULPrototypeCache::CollectMemoryReports.
RefPtr<StyleSheet> sheet = CloneSheet(*aData.mSheet);
if (dom::IsChromeURI(aData.mURI)) {
nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance();
if (cache && cache->IsEnabled()) {
if (!cache->GetStyleSheet(aData.mURI)) {
LOG((" Putting sheet in XUL prototype cache"));
NS_ASSERTION(sheet->IsComplete(),
"Should only be caching complete sheets");
// NOTE: If we stop cloning sheets before insertion, we need to change
// nsXULPrototypeCache::CollectMemoryReports() to stop using
// SizeOfIncludingThis() because it will no longer own the sheets.
cache->PutStyleSheet(std::move(sheet));
}
}
} 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) {
if (!key.KeyEquals(entry.GetKey())) {
MOZ_DIAGNOSTIC_ASSERT(entry.GetData().mSheet != sheet,
"Same sheet, different keys?");
} else {
MOZ_ASSERT(
entry.GetData().Expired() || aData.mLoader->ShouldBypassCache(),
"Overriding existing complete entry?");
}
}
#endif
UniquePtr<StyleUseCounters> counters;
if (aData.mUseCounters) {
// TODO(emilio): Servo_UseCounters_Clone() or something?
counters = Servo_UseCounters_Create().Consume();
Servo_UseCounters_Merge(counters.get(), aData.mUseCounters.get());
}
mCompleteSheets.InsertOrUpdate(
key, CompleteSheet{aData.mExpirationTime, std::move(counters),
std::move(sheet)});
}
}
void SharedStyleSheetCache::StartDeferredLoadsForLoader(
css::Loader& aLoader, StartLoads aStartLoads) {
using PendingLoad = css::Loader::PendingLoad;
LoadDataArray arr;
for (auto iter = mPendingDatas.Iter(); !iter.Done(); iter.Next()) {
bool startIt = false;
SheetLoadData* data = iter.Data();
do {
if (data->mLoader == &aLoader) {
// Note that we don't want to affect what the selected style set is, so
// use true for aHasAlternateRel.
if (aStartLoads != StartLoads::IfNonAlternate ||
aLoader.IsAlternateSheet(iter.Data()->mTitle, true) !=
IsAlternate::Yes) {
startIt = true;
break;
}
}
} while ((data = data->mNext));
if (startIt) {
arr.AppendElement(std::move(iter.Data()));
iter.Remove();
}
}
for (auto& data : arr) {
WillStartPendingLoad(*data);
data->mLoader->LoadSheet(*data, SheetState::NeedsParser, PendingLoad::Yes);
}
}
void SharedStyleSheetCache::CancelDeferredLoadsForLoader(css::Loader& aLoader) {
LoadDataArray arr;
for (auto iter = mPendingDatas.Iter(); !iter.Done(); iter.Next()) {
RefPtr<SheetLoadData>& first = iter.Data();
SheetLoadData* prev = nullptr;
SheetLoadData* current = iter.Data();
do {
if (current->mLoader != &aLoader) {
prev = current;
current = current->mNext;
continue;
}
// Detach the load from the list, mark it as cancelled, and then below
// call SheetComplete on it.
RefPtr<SheetLoadData> strong =
prev ? std::move(prev->mNext) : std::move(first);
MOZ_ASSERT(strong == current);
if (prev) {
prev->mNext = std::move(strong->mNext);
current = prev->mNext;
} else {
first = std::move(strong->mNext);
current = first;
}
strong->mIsCancelled = true;
arr.AppendElement(std::move(strong));
} while (current);
if (!first) {
iter.Remove();
}
}
for (auto& data : arr) {
aLoader.SheetComplete(*data, NS_BINDING_ABORTED);
}
}
void SharedStyleSheetCache::CancelLoadsForLoader(css::Loader& aLoader) {
CancelDeferredLoadsForLoader(aLoader);
// We can't stop in-progress loads because some other loader may care about
// them.
for (SheetLoadData* data : mLoadingDatas.Values()) {
MOZ_DIAGNOSTIC_ASSERT(data,
"We weren't properly notified and the load was "
"incorrectly dropped on the floor");
for (; data; data = data->mNext) {
if (data->mLoader == &aLoader) {
data->mIsCancelled = true;
}
}
}
}
void SharedStyleSheetCache::RegisterLoader(css::Loader& aLoader) {
MOZ_ASSERT(aLoader.GetDocument());
mLoaderPrincipalRefCnt.LookupOrInsert(aLoader.GetDocument()->NodePrincipal(),
0) += 1;
}
void SharedStyleSheetCache::UnregisterLoader(css::Loader& aLoader) {
MOZ_ASSERT(aLoader.GetDocument());
nsIPrincipal* prin = aLoader.GetDocument()->NodePrincipal();
auto lookup = mLoaderPrincipalRefCnt.Lookup(prin);
MOZ_RELEASE_ASSERT(lookup);
MOZ_RELEASE_ASSERT(lookup.Data());
if (!--lookup.Data()) {
lookup.Remove();
// TODO(emilio): Do this off a timer or something maybe.
for (auto iter = mCompleteSheets.Iter(); !iter.Done(); iter.Next()) {
if (iter.Key().LoaderPrincipal()->Equals(prin)) {
iter.Remove();
}
}
}
}
} // namespace mozilla
#undef LOG