1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "SharedStyleSheetCache.h"
9 #include "mozilla/MemoryReporting.h"
10 #include "mozilla/StyleSheet.h"
11 #include "mozilla/css/SheetLoadData.h"
12 #include "mozilla/dom/ContentParent.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/ServoBindings.h"
15 #include "nsContentUtils.h"
16 #include "nsXULPrototypeCache.h"
18 extern mozilla::LazyLogModule sCssLoaderLog
;
20 #define LOG(args) MOZ_LOG(sCssLoaderLog, mozilla::LogLevel::Debug, args)
24 using css::SheetLoadData
;
25 using SheetState
= css::Loader::SheetState
;
26 using LoadDataArray
= css::Loader::LoadDataArray
;
27 using IsAlternate
= css::Loader::IsAlternate
;
29 SharedStyleSheetCache
* SharedStyleSheetCache::sInstance
;
31 void SharedStyleSheetCache::Clear(nsIPrincipal
* aForPrincipal
) {
32 using ContentParent
= dom::ContentParent
;
34 if (XRE_IsParentProcess()) {
35 auto forPrincipal
= aForPrincipal
? Some(RefPtr(aForPrincipal
)) : Nothing();
36 for (auto* cp
: ContentParent::AllProcesses(ContentParent::eLive
)) {
37 Unused
<< cp
->SendClearStyleSheetCache(forPrincipal
);
46 sInstance
->mCompleteSheets
.Clear();
50 for (auto iter
= sInstance
->mCompleteSheets
.Iter(); !iter
.Done();
52 if (iter
.Key().Principal()->Equals(aForPrincipal
)) {
58 already_AddRefed
<SharedStyleSheetCache
> SharedStyleSheetCache::Create() {
59 MOZ_DIAGNOSTIC_ASSERT(!sInstance
);
60 RefPtr
<SharedStyleSheetCache
> cache
= new SharedStyleSheetCache();
61 sInstance
= cache
.get();
62 RegisterWeakMemoryReporter(cache
.get());
63 return cache
.forget();
66 SharedStyleSheetCache::~SharedStyleSheetCache() {
67 MOZ_DIAGNOSTIC_ASSERT(sInstance
== this);
68 UnregisterWeakMemoryReporter(this);
72 NS_IMPL_ISUPPORTS(SharedStyleSheetCache
, nsIMemoryReporter
)
74 MOZ_DEFINE_MALLOC_SIZE_OF(SharedStyleSheetCacheMallocSizeOf
)
77 SharedStyleSheetCache::CollectReports(nsIHandleReportCallback
* aHandleReport
,
78 nsISupports
* aData
, bool aAnonymize
) {
79 MOZ_COLLECT_REPORT("explicit/layout/style-sheet-cache/document-shared",
80 KIND_HEAP
, UNITS_BYTES
,
81 SizeOfIncludingThis(SharedStyleSheetCacheMallocSizeOf
),
82 "Memory used for SharedStyleSheetCache to share style "
83 "sheets across documents (not to be confused with "
84 "GlobalStyleSheetCache)");
88 static RefPtr
<StyleSheet
> CloneSheet(StyleSheet
& aSheet
) {
89 return aSheet
.Clone(nullptr, nullptr, nullptr, nullptr);
92 static void AssertComplete(const StyleSheet
& aSheet
) {
93 // This sheet came from the XUL cache or SharedStyleSheetCache; it better be a
95 MOZ_ASSERT(aSheet
.IsComplete(),
96 "Sheet thinks it's not complete while we think it is");
99 static void AssertIncompleteSheetMatches(const SheetLoadData
& aData
,
100 const SheetLoadDataHashKey
& aKey
) {
101 MOZ_ASSERT(aKey
.Principal()->Equals(aData
.mTriggeringPrincipal
),
102 "Principals should be the same");
103 MOZ_ASSERT(!aData
.mSheet
->HasForcedUniqueInner(),
104 "CSSOM shouldn't allow access to incomplete sheets");
107 bool SharedStyleSheetCache::CompleteSheet::Expired() const {
108 return mExpirationTime
&&
109 mExpirationTime
<= nsContentUtils::SecondsFromPRTime(PR_Now());
112 SharedStyleSheetCache::CacheResult
SharedStyleSheetCache::Lookup(
113 css::Loader
& aLoader
, const SheetLoadDataHashKey
& aKey
, bool aSyncLoad
) {
114 nsIURI
* uri
= aKey
.URI();
115 LOG(("SharedStyleSheetCache::Lookup(%s)", uri
->GetSpecOrDefault().get()));
117 // Try to find first in the XUL prototype cache.
118 if (dom::IsChromeURI(uri
)) {
119 nsXULPrototypeCache
* cache
= nsXULPrototypeCache::GetInstance();
120 if (cache
&& cache
->IsEnabled()) {
121 if (StyleSheet
* sheet
= cache
->GetStyleSheet(uri
)) {
122 LOG((" From XUL cache: %p", sheet
));
123 AssertComplete(*sheet
);
125 // See below, we always clone on insertion so we can guarantee the
126 // stylesheet is not modified.
127 MOZ_ASSERT(!sheet
->HasForcedUniqueInner());
129 // We need to check the parsing mode manually because the XUL cache only
130 // keys off the URI. But we should fix that!
131 if (sheet
->ParsingMode() == aKey
.ParsingMode()) {
132 aLoader
.DidHitCompleteSheetCache(aKey
, nullptr);
133 return {CloneSheet(*sheet
), SheetState::Complete
};
136 LOG((" Not cloning due to mismatched parsing mode"));
141 // Now complete sheets.
142 if (auto lookup
= mCompleteSheets
.Lookup(aKey
)) {
143 const CompleteSheet
& completeSheet
= lookup
.Data();
144 // We can assert the stylesheet has not been modified, as we clone it on
146 StyleSheet
& cachedSheet
= *completeSheet
.mSheet
;
147 LOG((" From completed: %p, bypass: %d, expired: %d", &cachedSheet
,
148 aLoader
.ShouldBypassCache(), completeSheet
.Expired()));
150 if ((!aLoader
.ShouldBypassCache() && !completeSheet
.Expired()) ||
151 aLoader
.mLoadsPerformed
.Contains(aKey
)) {
153 (" Not expired yet, or previously loaded already in "
156 AssertComplete(cachedSheet
);
157 MOZ_ASSERT(cachedSheet
.ParsingMode() == aKey
.ParsingMode());
158 MOZ_ASSERT(!cachedSheet
.HasForcedUniqueInner());
159 MOZ_ASSERT(!cachedSheet
.HasModifiedRules());
161 RefPtr
<StyleSheet
> clone
= CloneSheet(cachedSheet
);
162 MOZ_ASSERT(!clone
->HasForcedUniqueInner());
163 MOZ_ASSERT(!clone
->HasModifiedRules());
165 aLoader
.DidHitCompleteSheetCache(aKey
, completeSheet
.mUseCounters
.get());
166 return {std::move(clone
), SheetState::Complete
};
174 if (SheetLoadData
* data
= mLoadingDatas
.Get(aKey
)) {
175 LOG((" From loading: %p", data
->mSheet
.get()));
176 AssertIncompleteSheetMatches(*data
, aKey
);
177 return {CloneSheet(*data
->mSheet
), SheetState::Loading
};
180 if (SheetLoadData
* data
= mPendingDatas
.GetWeak(aKey
)) {
181 LOG((" From pending: %p", data
->mSheet
.get()));
182 AssertIncompleteSheetMatches(*data
, aKey
);
183 return {CloneSheet(*data
->mSheet
), SheetState::Pending
};
189 void SharedStyleSheetCache::WillStartPendingLoad(SheetLoadData
& aData
) {
190 SheetLoadData
* curr
= &aData
;
192 MOZ_DIAGNOSTIC_ASSERT(curr
->mLoader
->mPendingLoadCount
,
193 "Where did this pending load come from?");
194 --curr
->mLoader
->mPendingLoadCount
;
195 } while ((curr
= curr
->mNext
));
198 bool SharedStyleSheetCache::CoalesceLoad(const SheetLoadDataHashKey
& aKey
,
199 SheetLoadData
& aNewLoad
,
200 SheetState aExistingLoadState
) {
201 MOZ_ASSERT(SheetLoadDataHashKey(aNewLoad
).KeyEquals(aKey
));
202 SheetLoadData
* existingData
= nullptr;
203 if (aExistingLoadState
== SheetState::Loading
) {
204 existingData
= mLoadingDatas
.Get(aKey
);
205 MOZ_ASSERT(existingData
, "CreateSheet lied about the state");
206 } else if (aExistingLoadState
== SheetState::Pending
) {
207 existingData
= mPendingDatas
.GetWeak(aKey
);
208 MOZ_ASSERT(existingData
, "CreateSheet lied about the state");
215 if (aExistingLoadState
== SheetState::Pending
&& !aNewLoad
.ShouldDefer()) {
216 // Kick the load off; someone cares about it right away
217 RefPtr
<SheetLoadData
> removedData
;
218 mPendingDatas
.Remove(aKey
, getter_AddRefs(removedData
));
219 MOZ_ASSERT(removedData
== existingData
, "Bad loading table");
221 WillStartPendingLoad(*removedData
);
223 // We insert to the front instead of the back, to keep the invariant that
224 // the front sheet always is the one that triggers the load.
225 aNewLoad
.mNext
= std::move(removedData
);
226 LOG((" Forcing load of pending data"));
230 LOG((" Glomming on to existing load"));
231 SheetLoadData
* data
= existingData
;
232 while (data
->mNext
) {
235 data
->mNext
= &aNewLoad
;
240 size_t SharedStyleSheetCache::SizeOfIncludingThis(
241 MallocSizeOf aMallocSizeOf
) const {
242 size_t n
= aMallocSizeOf(this);
244 n
+= mCompleteSheets
.ShallowSizeOfExcludingThis(aMallocSizeOf
);
245 for (const auto& data
: mCompleteSheets
.Values()) {
246 n
+= data
.mSheet
->SizeOfIncludingThis(aMallocSizeOf
);
247 n
+= aMallocSizeOf(data
.mUseCounters
.get());
250 // Measurement of the following members may be added later if DMD finds it is
252 // - mLoadingDatas: transient, and should be small
253 // - mPendingDatas: transient, and should be small
257 void SharedStyleSheetCache::DeferSheetLoad(const SheetLoadDataHashKey
& aKey
,
258 SheetLoadData
& aData
) {
259 MOZ_ASSERT(SheetLoadDataHashKey(aData
).KeyEquals(aKey
));
260 MOZ_DIAGNOSTIC_ASSERT(!aData
.mNext
, "Should only defer loads once");
262 aData
.mMustNotify
= true;
263 mPendingDatas
.InsertOrUpdate(aKey
, RefPtr
{&aData
});
266 void SharedStyleSheetCache::LoadStarted(const SheetLoadDataHashKey
& aKey
,
267 SheetLoadData
& aData
) {
268 MOZ_ASSERT(aData
.mURI
, "No load required?");
269 MOZ_ASSERT(!aData
.mIsLoading
, "Already loading? How?");
270 MOZ_ASSERT(SheetLoadDataHashKey(aData
).KeyEquals(aKey
));
271 aData
.mIsLoading
= true;
272 mLoadingDatas
.InsertOrUpdate(aKey
, &aData
);
275 void SharedStyleSheetCache::LoadCompleted(SharedStyleSheetCache
* aCache
,
276 SheetLoadData
& aData
,
278 // If aStatus is a failure we need to mark this data failed. We also need to
279 // mark any ancestors of a failing data as failed and any sibling of a
280 // failing data as failed. Note that SheetComplete is never called on a
281 // SheetLoadData that is the mNext of some other SheetLoadData.
282 nsresult cancelledStatus
= aStatus
;
283 if (NS_FAILED(aStatus
)) {
284 css::Loader::MarkLoadTreeFailed(aData
);
286 cancelledStatus
= NS_BINDING_ABORTED
;
287 SheetLoadData
* data
= &aData
;
289 if (data
->mIsCancelled
) {
290 // We only need to mark loads for this loader as cancelled, so as to not
291 // fire error events in unrelated documents.
292 css::Loader::MarkLoadTreeFailed(*data
, data
->mLoader
);
294 } while ((data
= data
->mNext
));
297 // 8 is probably big enough for all our common cases. It's not likely that
298 // imports will nest more than 8 deep, and multiple sheets with the same URI
300 AutoTArray
<RefPtr
<SheetLoadData
>, 8> datasToNotify
;
301 LoadCompletedInternal(aCache
, aData
, datasToNotify
);
303 // Now it's safe to go ahead and notify observers
304 for (RefPtr
<SheetLoadData
>& data
: datasToNotify
) {
305 auto status
= data
->mIsCancelled
? cancelledStatus
: aStatus
;
306 data
->mLoader
->NotifyObservers(*data
, status
);
310 void SharedStyleSheetCache::LoadCompletedInternal(
311 SharedStyleSheetCache
* aCache
, SheetLoadData
& aData
,
312 nsTArray
<RefPtr
<SheetLoadData
>>& aDatasToNotify
) {
313 if (aData
.mIsLoading
) {
315 SheetLoadDataHashKey
key(aData
);
316 Maybe
<SheetLoadData
*> loadingData
= aCache
->mLoadingDatas
.Extract(key
);
317 MOZ_DIAGNOSTIC_ASSERT(loadingData
);
318 MOZ_DIAGNOSTIC_ASSERT(loadingData
.value() == &aData
);
319 Unused
<< loadingData
;
320 aData
.mIsLoading
= false;
323 // Go through and deal with the whole linked list.
324 SheetLoadData
* data
= &aData
;
326 MOZ_DIAGNOSTIC_ASSERT(!data
->mSheetCompleteCalled
);
327 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
328 data
->mSheetCompleteCalled
= true;
331 if (!data
->mSheetAlreadyComplete
) {
332 // If mSheetAlreadyComplete, then the sheet could well be modified between
333 // when we posted the async call to SheetComplete and now, since the sheet
334 // was page-accessible during that whole time.
336 // HasForcedUniqueInner() is okay if the sheet is constructed, because
337 // constructed sheets are always unique and they may be set to complete
338 // multiple times if their rules are replaced via Replace()
339 MOZ_ASSERT(data
->mSheet
->IsConstructed() ||
340 !data
->mSheet
->HasForcedUniqueInner(),
341 "should not get a forced unique inner during parsing");
342 // Insert the sheet into the tree now the sheet has loaded, but only if
343 // the sheet is still relevant, and if this is a top-level sheet.
344 const bool needInsertIntoTree
= [&] {
345 if (!data
->mLoader
->GetDocument()) {
346 // Not a document load, nothing to do.
349 if (data
->IsPreload()) {
350 // Preloads are not supposed to be observable.
353 if (data
->mSheet
->IsConstructed()) {
354 // Constructable sheets are not in the regular stylesheet tree.
357 if (data
->mIsChildSheet
) {
358 // A child sheet, those will get exposed from the parent, no need to
359 // insert them into the tree.
362 if (data
->mOwningNode
!= data
->mSheet
->GetOwnerNode()) {
363 // The sheet was already removed from the tree and is no longer the
364 // current sheet of the owning node, we can bail.
370 if (needInsertIntoTree
) {
371 data
->mLoader
->InsertSheetInTree(*data
->mSheet
, data
->mOwningNode
);
373 data
->mSheet
->SetComplete();
374 data
->ScheduleLoadEventIfNeeded();
375 } else if (data
->mSheet
->IsApplicable()) {
376 if (dom::Document
* doc
= data
->mLoader
->GetDocument()) {
377 // We post these events for devtools, even though the applicable state
378 // has not actually changed, to make the cache not observable.
379 doc
->PostStyleSheetApplicableStateChangeEvent(*data
->mSheet
);
383 aDatasToNotify
.AppendElement(data
);
385 NS_ASSERTION(!data
->mParentData
|| data
->mParentData
->mPendingChildren
!= 0,
386 "Broken pending child count on our parent");
388 // If we have a parent, our parent is no longer being parsed, and
389 // we are the last pending child, then our load completion
390 // completes the parent too. Note that the parent _can_ still be
391 // being parsed (eg if the child (us) failed to open the channel
393 if (data
->mParentData
&& --(data
->mParentData
->mPendingChildren
) == 0 &&
394 !data
->mParentData
->mIsBeingParsed
) {
395 LoadCompletedInternal(aCache
, *data
->mParentData
, aDatasToNotify
);
402 aCache
->InsertIntoCompleteCacheIfNeeded(aData
);
406 void SharedStyleSheetCache::InsertIntoCompleteCacheIfNeeded(
407 SheetLoadData
& aData
) {
408 MOZ_ASSERT(aData
.mLoader
->GetDocument(),
409 "We only cache document-associated sheets");
410 LOG(("SharedStyleSheetCache::InsertIntoCompleteCacheIfNeeded"));
411 // If we ever start doing this for failed loads, we'll need to adjust the
412 // PostLoadEvent code that thinks anything already complete must have loaded
414 if (aData
.mLoadFailed
) {
415 LOG((" Load failed, bailing"));
419 // If this sheet came from the cache already, there's no need to override
421 if (aData
.mSheetAlreadyComplete
) {
422 LOG((" Sheet came from the cache, bailing"));
427 LOG((" Inline or constructable style sheet, bailing"));
428 // Inline sheet caching happens in Loader::mInlineSheets.
429 // Constructable sheets are not worth caching, they're always unique.
433 // We need to clone the sheet on insertion to the cache because otherwise the
434 // stylesheets can keep alive full windows alive via either their JS wrapper,
435 // or via StyleSheet::mRelevantGlobal.
437 // If this ever changes, then you also need to fix up the memory reporting in
438 // both SizeOfIncludingThis and nsXULPrototypeCache::CollectMemoryReports.
439 RefPtr
<StyleSheet
> sheet
= CloneSheet(*aData
.mSheet
);
441 if (dom::IsChromeURI(aData
.mURI
)) {
442 nsXULPrototypeCache
* cache
= nsXULPrototypeCache::GetInstance();
443 if (cache
&& cache
->IsEnabled()) {
444 if (!cache
->GetStyleSheet(aData
.mURI
)) {
445 LOG((" Putting sheet in XUL prototype cache"));
446 NS_ASSERTION(sheet
->IsComplete(),
447 "Should only be caching complete sheets");
449 // NOTE: If we stop cloning sheets before insertion, we need to change
450 // nsXULPrototypeCache::CollectMemoryReports() to stop using
451 // SizeOfIncludingThis() because it will no longer own the sheets.
452 cache
->PutStyleSheet(std::move(sheet
));
456 LOG((" Putting style sheet in shared cache: %s",
457 aData
.mURI
->GetSpecOrDefault().get()));
458 SheetLoadDataHashKey
key(aData
);
459 MOZ_ASSERT(sheet
->IsComplete(), "Should only be caching complete sheets");
461 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
462 for (const auto& entry
: mCompleteSheets
) {
463 if (!key
.KeyEquals(entry
.GetKey())) {
464 MOZ_DIAGNOSTIC_ASSERT(entry
.GetData().mSheet
!= sheet
,
465 "Same sheet, different keys?");
468 entry
.GetData().Expired() || aData
.mLoader
->ShouldBypassCache(),
469 "Overriding existing complete entry?");
474 UniquePtr
<StyleUseCounters
> counters
;
475 if (aData
.mUseCounters
) {
476 // TODO(emilio): Servo_UseCounters_Clone() or something?
477 counters
= Servo_UseCounters_Create().Consume();
478 Servo_UseCounters_Merge(counters
.get(), aData
.mUseCounters
.get());
481 mCompleteSheets
.InsertOrUpdate(
482 key
, CompleteSheet
{aData
.mExpirationTime
, std::move(counters
),
487 void SharedStyleSheetCache::StartDeferredLoadsForLoader(
488 css::Loader
& aLoader
, StartLoads aStartLoads
) {
489 using PendingLoad
= css::Loader::PendingLoad
;
492 for (auto iter
= mPendingDatas
.Iter(); !iter
.Done(); iter
.Next()) {
493 bool startIt
= false;
494 SheetLoadData
* data
= iter
.Data();
496 if (data
->mLoader
== &aLoader
) {
497 // Note that we don't want to affect what the selected style set is, so
498 // use true for aHasAlternateRel.
499 if (aStartLoads
!= StartLoads::IfNonAlternate
||
500 aLoader
.IsAlternateSheet(iter
.Data()->mTitle
, true) !=
506 } while ((data
= data
->mNext
));
508 arr
.AppendElement(std::move(iter
.Data()));
512 for (auto& data
: arr
) {
513 WillStartPendingLoad(*data
);
514 data
->mLoader
->LoadSheet(*data
, SheetState::NeedsParser
, PendingLoad::Yes
);
518 void SharedStyleSheetCache::CancelDeferredLoadsForLoader(css::Loader
& aLoader
) {
521 for (auto iter
= mPendingDatas
.Iter(); !iter
.Done(); iter
.Next()) {
522 RefPtr
<SheetLoadData
>& first
= iter
.Data();
523 SheetLoadData
* prev
= nullptr;
524 SheetLoadData
* current
= iter
.Data();
526 if (current
->mLoader
!= &aLoader
) {
528 current
= current
->mNext
;
531 // Detach the load from the list, mark it as cancelled, and then below
532 // call SheetComplete on it.
533 RefPtr
<SheetLoadData
> strong
=
534 prev
? std::move(prev
->mNext
) : std::move(first
);
535 MOZ_ASSERT(strong
== current
);
537 prev
->mNext
= std::move(strong
->mNext
);
538 current
= prev
->mNext
;
540 first
= std::move(strong
->mNext
);
543 strong
->mIsCancelled
= true;
544 arr
.AppendElement(std::move(strong
));
552 for (auto& data
: arr
) {
553 aLoader
.SheetComplete(*data
, NS_BINDING_ABORTED
);
557 void SharedStyleSheetCache::CancelLoadsForLoader(css::Loader
& aLoader
) {
558 CancelDeferredLoadsForLoader(aLoader
);
560 // We can't stop in-progress loads because some other loader may care about
562 for (SheetLoadData
* data
: mLoadingDatas
.Values()) {
563 MOZ_DIAGNOSTIC_ASSERT(data
,
564 "We weren't properly notified and the load was "
565 "incorrectly dropped on the floor");
566 for (; data
; data
= data
->mNext
) {
567 if (data
->mLoader
== &aLoader
) {
568 data
->mIsCancelled
= true;
574 void SharedStyleSheetCache::RegisterLoader(css::Loader
& aLoader
) {
575 MOZ_ASSERT(aLoader
.GetDocument());
576 mLoaderPrincipalRefCnt
.LookupOrInsert(aLoader
.GetDocument()->NodePrincipal(),
580 void SharedStyleSheetCache::UnregisterLoader(css::Loader
& aLoader
) {
581 MOZ_ASSERT(aLoader
.GetDocument());
582 nsIPrincipal
* prin
= aLoader
.GetDocument()->NodePrincipal();
583 auto lookup
= mLoaderPrincipalRefCnt
.Lookup(prin
);
584 MOZ_RELEASE_ASSERT(lookup
);
585 MOZ_RELEASE_ASSERT(lookup
.Data());
586 if (!--lookup
.Data()) {
588 // TODO(emilio): Do this off a timer or something maybe.
589 for (auto iter
= mCompleteSheets
.Iter(); !iter
.Done(); iter
.Next()) {
590 if (iter
.Key().LoaderPrincipal()->Equals(prin
)) {
597 } // namespace mozilla