SharedSubResourceCache.h (19976B)
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/. */ 6 7 #ifndef mozilla_SharedSubResourceCache_h__ 8 #define mozilla_SharedSubResourceCache_h__ 9 10 // A cache that allows us to share subresources across documents. In order to 11 // use it you need to provide some types, mainly: 12 // 13 // * Loader, which implements LoaderPrincipal() and allows you to key per 14 // principal. The idea is that this would be the 15 // {CSS,Script,Image}Loader object. 16 // 17 // * Key (self explanatory). We might want to introduce a common key to 18 // share the cache partitioning logic. 19 // 20 // * Value, which represents the final cached value. This is expected to 21 // be a StyleSheet / Stencil / imgRequestProxy. 22 // 23 // * LoadingValue, which must inherit from 24 // SharedSubResourceCacheLoadingValueBase (which contains the linked 25 // list and the state that the cache manages). It also must provide a 26 // ValueForCache() and ExpirationTime() members. For style, this is the 27 // SheetLoadData. 28 29 #include "mozilla/MemoryReporting.h" 30 #include "mozilla/PrincipalHashKey.h" 31 #include "mozilla/RefPtr.h" 32 #include "mozilla/StaticPtr.h" 33 #include "mozilla/StoragePrincipalHelper.h" 34 #include "mozilla/TimeStamp.h" 35 #include "mozilla/WeakPtr.h" 36 #include "mozilla/dom/CacheExpirationTime.h" 37 #include "mozilla/dom/CacheablePerformanceTimingData.h" 38 #include "mozilla/dom/Document.h" 39 #include "nsContentUtils.h" 40 #include "nsIMemoryReporter.h" 41 #include "nsISupportsImpl.h" 42 #include "nsRefPtrHashtable.h" 43 #include "nsTHashMap.h" 44 45 namespace mozilla { 46 47 namespace net { 48 class nsHttpResponseHead; 49 } 50 51 // A struct to hold the network-related metadata associated with the cache. 52 // 53 // When inserting a cache, the consumer should create this from the request and 54 // make it available via 55 // SharedSubResourceCacheLoadingValueBase::GetNetworkMetadata. 56 // 57 // When using a cache, the consumer can retrieve this from 58 // SharedSubResourceCache::Result::mNetworkMetadata and use it for notifying 59 // the observers once the necessary data becomes ready. 60 // This struct is ref-counted in order to allow this usage. 61 class SubResourceNetworkMetadataHolder { 62 public: 63 SubResourceNetworkMetadataHolder() = delete; 64 65 explicit SubResourceNetworkMetadataHolder(nsIRequest* aRequest); 66 67 const dom::CacheablePerformanceTimingData* GetPerfData() const { 68 return mPerfData.ptrOr(nullptr); 69 } 70 71 const net::nsHttpResponseHead* GetResponseHead() const { 72 return mResponseHead.get(); 73 } 74 75 NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SubResourceNetworkMetadataHolder) 76 77 private: 78 ~SubResourceNetworkMetadataHolder(); 79 80 mozilla::Maybe<dom::CacheablePerformanceTimingData> mPerfData; 81 mozilla::UniquePtr<net::nsHttpResponseHead> mResponseHead; 82 }; 83 84 enum class CachedSubResourceState { 85 Miss, 86 Loading, 87 Pending, 88 Complete, 89 }; 90 91 template <typename Derived> 92 struct SharedSubResourceCacheLoadingValueBase { 93 // Whether we're in the "loading" hash table. 94 RefPtr<Derived> mNext; 95 96 virtual bool IsLoading() const = 0; 97 virtual bool IsCancelled() const = 0; 98 virtual bool IsSyncLoad() const = 0; 99 100 virtual SubResourceNetworkMetadataHolder* GetNetworkMetadata() const = 0; 101 102 virtual void StartLoading() = 0; 103 virtual void SetLoadCompleted() = 0; 104 virtual void OnCoalescedTo(const Derived& aExistingLoad) = 0; 105 virtual void Cancel() = 0; 106 107 // Return the next sub-resource which has the same key. 108 Derived* GetNextSubResource() { return mNext; } 109 110 ~SharedSubResourceCacheLoadingValueBase() { 111 // Do this iteratively to avoid blowing up the stack. 112 RefPtr<Derived> next = std::move(mNext); 113 while (next) { 114 next = std::move(next->mNext); 115 } 116 } 117 }; 118 119 namespace SharedSubResourceCacheUtils { 120 121 void AddPerformanceEntryForCache( 122 const nsString& aEntryName, const nsString& aInitiatorType, 123 const SubResourceNetworkMetadataHolder* aNetworkMetadata, 124 TimeStamp aStartTime, TimeStamp aEndTime, dom::Document* aDocument); 125 126 bool ShouldClearEntry(nsIURI* aEntryURI, nsIPrincipal* aEntryLoaderPrincipal, 127 nsIPrincipal* aEntryPartitionPrincipal, 128 const Maybe<bool>& aChrome, 129 const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal, 130 const Maybe<nsCString>& aSchemelessSite, 131 const Maybe<OriginAttributesPattern>& aPattern, 132 const Maybe<nsCString>& aURL); 133 134 } // namespace SharedSubResourceCacheUtils 135 136 template <typename Traits, typename Derived> 137 class SharedSubResourceCache { 138 private: 139 using Loader = typename Traits::Loader; 140 using Key = typename Traits::Key; 141 using Value = typename Traits::Value; 142 using LoadingValue = typename Traits::LoadingValue; 143 static Key KeyFromLoadingValue(const LoadingValue& aValue) { 144 return Traits::KeyFromLoadingValue(aValue); 145 } 146 147 const Derived& AsDerived() const { 148 return *static_cast<const Derived*>(this); 149 } 150 Derived& AsDerived() { return *static_cast<Derived*>(this); } 151 152 public: 153 SharedSubResourceCache(const SharedSubResourceCache&) = delete; 154 SharedSubResourceCache(SharedSubResourceCache&&) = delete; 155 SharedSubResourceCache() = default; 156 157 static Derived* Get() { 158 static_assert( 159 std::is_base_of_v<SharedSubResourceCacheLoadingValueBase<LoadingValue>, 160 LoadingValue>); 161 162 if (sSingleton) { 163 return sSingleton.get(); 164 } 165 MOZ_DIAGNOSTIC_ASSERT(!sSingleton); 166 sSingleton = new Derived(); 167 sSingleton->Init(); 168 return sSingleton.get(); 169 } 170 171 static void DeleteSingleton() { sSingleton = nullptr; } 172 173 protected: 174 struct CompleteSubResource { 175 RefPtr<Value> mResource; 176 RefPtr<SubResourceNetworkMetadataHolder> mNetworkMetadata; 177 CacheExpirationTime mExpirationTime = CacheExpirationTime::Never(); 178 bool mWasSyncLoad = false; 179 180 explicit CompleteSubResource(LoadingValue& aValue) 181 : mResource(aValue.ValueForCache()), 182 mNetworkMetadata(aValue.GetNetworkMetadata()), 183 mExpirationTime(aValue.ExpirationTime()), 184 mWasSyncLoad(aValue.IsSyncLoad()) {} 185 186 inline bool Expired() const; 187 }; 188 189 public: 190 struct Result { 191 Value* mCompleteValue = nullptr; 192 RefPtr<SubResourceNetworkMetadataHolder> mNetworkMetadata; 193 194 LoadingValue* mLoadingOrPendingValue = nullptr; 195 CachedSubResourceState mState = CachedSubResourceState::Miss; 196 197 constexpr Result() = default; 198 199 explicit constexpr Result(const CompleteSubResource& aCompleteSubResource) 200 : mCompleteValue(aCompleteSubResource.mResource.get()), 201 mNetworkMetadata(aCompleteSubResource.mNetworkMetadata), 202 mLoadingOrPendingValue(nullptr), 203 mState(CachedSubResourceState::Complete) {} 204 205 constexpr Result(LoadingValue* aLoadingOrPendingValue, 206 CachedSubResourceState aState) 207 : mLoadingOrPendingValue(aLoadingOrPendingValue), mState(aState) {} 208 }; 209 210 Result Lookup(Loader&, const Key&, bool aSyncLoad); 211 212 // Tries to coalesce with an already existing load. The sheet state must be 213 // the one that Lookup returned, if it returned a sheet. 214 // 215 // TODO(emilio): Maybe try to merge this with the lookup? Most consumers could 216 // have a data there already. 217 [[nodiscard]] bool CoalesceLoad(const Key&, LoadingValue& aNewLoad, 218 CachedSubResourceState aExistingLoadState); 219 220 size_t SizeOfExcludingThis(MallocSizeOf) const; 221 222 // Puts the load into the "loading" set. 223 void LoadStarted(const Key&, LoadingValue&); 224 225 // Removes the load from the "loading" set if there. 226 void LoadCompleted(LoadingValue&); 227 228 // Inserts a value into the cache. 229 void Insert(LoadingValue&); 230 231 // Evict the specific cache. 232 void Evict(const Key&); 233 234 // Puts a load into the "pending" set. 235 void DeferLoad(const Key&, LoadingValue&); 236 237 template <typename Callback> 238 void StartPendingLoadsForLoader(Loader&, const Callback& aShouldStartLoad); 239 void CancelLoadsForLoader(Loader&); 240 241 // Register a loader into the cache. This has the effect of keeping alive all 242 // subresources for the origin of the loader's document until UnregisterLoader 243 // is called. 244 void RegisterLoader(Loader&); 245 246 // Unregister a loader from the cache. 247 // 248 // If this is the loader for the last document of a given origin, then all the 249 // subresources for that document will be removed from the cache. This needs 250 // to be called when the document goes away, or when its principal changes. 251 void UnregisterLoader(Loader&); 252 253 void PrepareForShutdown(); 254 255 void ClearInProcess(const Maybe<bool>& aChrome, 256 const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal, 257 const Maybe<nsCString>& aSchemelessSite, 258 const Maybe<OriginAttributesPattern>& aPattern, 259 const Maybe<nsCString>& aURL); 260 261 protected: 262 void CancelPendingLoadsForLoader(Loader&); 263 264 void WillStartPendingLoad(LoadingValue&); 265 266 void EvictPrincipal(nsIPrincipal*); 267 268 nsTHashMap<Key, CompleteSubResource> mComplete; 269 nsRefPtrHashtable<Key, LoadingValue> mPending; 270 // The SheetLoadData pointers in mLoadingDatas below are weak references that 271 // get cleaned up when StreamLoader::OnStopRequest gets called. 272 // 273 // Note that we hold on to all sheet loads, even if in the end they happen not 274 // to be cacheable. 275 nsTHashMap<Key, WeakPtr<LoadingValue>> mLoading; 276 277 // An origin-to-number-of-registered-documents count, in order to manage cache 278 // eviction as described in RegisterLoader / UnregisterLoader. 279 nsTHashMap<PrincipalHashKey, uint32_t> mLoaderPrincipalRefCnt; 280 281 protected: 282 // Lazily created in the first Get() call. 283 // The singleton should be deleted by DeleteSingleton() during shutdown. 284 inline static MOZ_GLOBINIT StaticRefPtr<Derived> sSingleton; 285 }; 286 287 template <typename Traits, typename Derived> 288 void SharedSubResourceCache<Traits, Derived>::ClearInProcess( 289 const Maybe<bool>& aChrome, const Maybe<nsCOMPtr<nsIPrincipal>>& aPrincipal, 290 const Maybe<nsCString>& aSchemelessSite, 291 const Maybe<OriginAttributesPattern>& aPattern, 292 const Maybe<nsCString>& aURL) { 293 MOZ_ASSERT(aSchemelessSite.isSome() == aPattern.isSome(), 294 "Must pass both site and OA pattern."); 295 296 if (!aChrome && !aPrincipal && !aSchemelessSite && !aURL) { 297 mComplete.Clear(); 298 return; 299 } 300 301 for (auto iter = mComplete.Iter(); !iter.Done(); iter.Next()) { 302 if (SharedSubResourceCacheUtils::ShouldClearEntry( 303 iter.Key().URI(), iter.Key().LoaderPrincipal(), 304 iter.Key().PartitionPrincipal(), aChrome, aPrincipal, 305 aSchemelessSite, aPattern, aURL)) { 306 iter.Remove(); 307 } 308 } 309 } 310 311 template <typename Traits, typename Derived> 312 void SharedSubResourceCache<Traits, Derived>::RegisterLoader(Loader& aLoader) { 313 mLoaderPrincipalRefCnt.LookupOrInsert(aLoader.LoaderPrincipal(), 0) += 1; 314 } 315 316 template <typename Traits, typename Derived> 317 void SharedSubResourceCache<Traits, Derived>::UnregisterLoader( 318 Loader& aLoader) { 319 nsIPrincipal* prin = aLoader.LoaderPrincipal(); 320 auto lookup = mLoaderPrincipalRefCnt.Lookup(prin); 321 MOZ_RELEASE_ASSERT(lookup); 322 MOZ_RELEASE_ASSERT(lookup.Data()); 323 if (!--lookup.Data()) { 324 lookup.Remove(); 325 // TODO(emilio): Do this off a timer or something maybe, though in practice 326 // BFCache is good enough at keeping things alive. 327 AsDerived().EvictPrincipal(prin); 328 } 329 } 330 331 template <typename Traits, typename Derived> 332 void SharedSubResourceCache<Traits, Derived>::EvictPrincipal( 333 nsIPrincipal* aPrincipal) { 334 for (auto iter = mComplete.Iter(); !iter.Done(); iter.Next()) { 335 if (iter.Key().LoaderPrincipal()->Equals(aPrincipal)) { 336 iter.Remove(); 337 } 338 } 339 } 340 341 template <typename Traits, typename Derived> 342 void SharedSubResourceCache<Traits, Derived>::CancelPendingLoadsForLoader( 343 Loader& aLoader) { 344 AutoTArray<RefPtr<LoadingValue>, 10> arr; 345 346 for (auto iter = mPending.Iter(); !iter.Done(); iter.Next()) { 347 RefPtr<LoadingValue>& first = iter.Data(); 348 LoadingValue* prev = nullptr; 349 LoadingValue* current = iter.Data(); 350 do { 351 if (¤t->Loader() != &aLoader) { 352 prev = current; 353 current = current->mNext; 354 continue; 355 } 356 // Detach the load from the list, mark it as cancelled, and then below 357 // call SheetComplete on it. 358 RefPtr<LoadingValue> strong = 359 prev ? std::move(prev->mNext) : std::move(first); 360 MOZ_ASSERT(strong == current); 361 if (prev) { 362 prev->mNext = std::move(strong->mNext); 363 current = prev->mNext; 364 } else { 365 first = std::move(strong->mNext); 366 current = first; 367 } 368 arr.AppendElement(std::move(strong)); 369 } while (current); 370 371 if (!first) { 372 iter.Remove(); 373 } 374 } 375 376 for (auto& loading : arr) { 377 loading->DidCancelLoad(); 378 } 379 } 380 381 template <typename Traits, typename Derived> 382 void SharedSubResourceCache<Traits, Derived>::WillStartPendingLoad( 383 LoadingValue& aData) { 384 LoadingValue* curr = &aData; 385 do { 386 curr->Loader().WillStartPendingLoad(); 387 } while ((curr = curr->mNext)); 388 } 389 390 template <typename Traits, typename Derived> 391 void SharedSubResourceCache<Traits, Derived>::CancelLoadsForLoader( 392 Loader& aLoader) { 393 CancelPendingLoadsForLoader(aLoader); 394 395 // We can't stop in-progress loads because some other loader may care about 396 // them. 397 for (LoadingValue* data : mLoading.Values()) { 398 MOZ_DIAGNOSTIC_ASSERT(data, 399 "We weren't properly notified and the load was " 400 "incorrectly dropped on the floor"); 401 for (; data; data = data->mNext) { 402 if (&data->Loader() == &aLoader) { 403 data->Cancel(); 404 MOZ_ASSERT(data->IsCancelled()); 405 } 406 } 407 } 408 } 409 410 template <typename Traits, typename Derived> 411 void SharedSubResourceCache<Traits, Derived>::DeferLoad(const Key& aKey, 412 LoadingValue& aValue) { 413 MOZ_ASSERT(KeyFromLoadingValue(aValue).KeyEquals(aKey)); 414 MOZ_DIAGNOSTIC_ASSERT(!aValue.mNext, "Should only defer loads once"); 415 416 mPending.InsertOrUpdate(aKey, RefPtr{&aValue}); 417 } 418 419 template <typename Traits, typename Derived> 420 template <typename Callback> 421 void SharedSubResourceCache<Traits, Derived>::StartPendingLoadsForLoader( 422 Loader& aLoader, const Callback& aShouldStartLoad) { 423 AutoTArray<RefPtr<LoadingValue>, 10> arr; 424 425 for (auto iter = mPending.Iter(); !iter.Done(); iter.Next()) { 426 bool startIt = false; 427 { 428 LoadingValue* data = iter.Data(); 429 do { 430 if (&data->Loader() == &aLoader) { 431 if (aShouldStartLoad(*data)) { 432 startIt = true; 433 break; 434 } 435 } 436 } while ((data = data->mNext)); 437 } 438 if (startIt) { 439 arr.AppendElement(std::move(iter.Data())); 440 iter.Remove(); 441 } 442 } 443 for (auto& data : arr) { 444 WillStartPendingLoad(*data); 445 data->StartPendingLoad(); 446 } 447 } 448 449 template <typename Traits, typename Derived> 450 void SharedSubResourceCache<Traits, Derived>::Insert(LoadingValue& aValue) { 451 auto key = KeyFromLoadingValue(aValue); 452 #ifdef DEBUG 453 // We only expect a complete entry to be overriding when: 454 // * It's expired. 455 // * We're explicitly bypassing the cache. 456 // * Our entry is a sync load that was completed after aValue started loading 457 // async. 458 for (const auto& entry : mComplete) { 459 if (key.KeyEquals(entry.GetKey())) { 460 MOZ_ASSERT(entry.GetData().Expired() || 461 aValue.Loader().ShouldBypassCache() || 462 (entry.GetData().mWasSyncLoad && !aValue.IsSyncLoad()), 463 "Overriding existing complete entry?"); 464 } 465 } 466 #endif 467 468 mComplete.InsertOrUpdate(key, CompleteSubResource(aValue)); 469 } 470 471 template <typename Traits, typename Derived> 472 void SharedSubResourceCache<Traits, Derived>::Evict(const Key& aKey) { 473 (void)mComplete.Remove(aKey); 474 } 475 476 template <typename Traits, typename Derived> 477 bool SharedSubResourceCache<Traits, Derived>::CoalesceLoad( 478 const Key& aKey, LoadingValue& aNewLoad, 479 CachedSubResourceState aExistingLoadState) { 480 MOZ_ASSERT(KeyFromLoadingValue(aNewLoad).KeyEquals(aKey)); 481 // TODO(emilio): If aExistingLoadState is inconvenient, we could get rid of it 482 // by paying two hash lookups... 483 LoadingValue* existingLoad = nullptr; 484 if (aExistingLoadState == CachedSubResourceState::Loading) { 485 existingLoad = mLoading.Get(aKey); 486 MOZ_ASSERT(existingLoad, "Caller lied about the state"); 487 } else if (aExistingLoadState == CachedSubResourceState::Pending) { 488 existingLoad = mPending.GetWeak(aKey); 489 MOZ_ASSERT(existingLoad, "Caller lied about the state"); 490 } 491 492 if (!existingLoad) { 493 return false; 494 } 495 496 if (aExistingLoadState == CachedSubResourceState::Pending && 497 !aNewLoad.ShouldDefer()) { 498 // Kick the load off; someone cares about it right away 499 RefPtr<LoadingValue> removedLoad; 500 mPending.Remove(aKey, getter_AddRefs(removedLoad)); 501 MOZ_ASSERT(removedLoad == existingLoad, "Bad loading table"); 502 503 WillStartPendingLoad(*removedLoad); 504 505 // We insert to the front instead of the back, to keep the invariant that 506 // the front sheet always is the one that triggers the load. 507 aNewLoad.mNext = std::move(removedLoad); 508 return false; 509 } 510 511 LoadingValue* data = existingLoad; 512 while (data->mNext) { 513 data = data->mNext; 514 } 515 data->mNext = &aNewLoad; 516 517 aNewLoad.OnCoalescedTo(*existingLoad); 518 return true; 519 } 520 521 template <typename Traits, typename Derived> 522 auto SharedSubResourceCache<Traits, Derived>::Lookup(Loader& aLoader, 523 const Key& aKey, 524 bool aSyncLoad) -> Result { 525 // Now complete sheets. 526 if (auto lookup = mComplete.Lookup(aKey)) { 527 const CompleteSubResource& completeSubResource = lookup.Data(); 528 if ((!aLoader.ShouldBypassCache() && !completeSubResource.Expired()) || 529 aLoader.HasLoaded(aKey)) { 530 return Result(completeSubResource); 531 } 532 } 533 534 if (aSyncLoad) { 535 return Result(); 536 } 537 538 if (LoadingValue* data = mLoading.Get(aKey)) { 539 return Result(data, CachedSubResourceState::Loading); 540 } 541 542 if (LoadingValue* data = mPending.GetWeak(aKey)) { 543 return Result(data, CachedSubResourceState::Pending); 544 } 545 546 return {}; 547 } 548 549 template <typename Traits, typename Derived> 550 size_t SharedSubResourceCache<Traits, Derived>::SizeOfExcludingThis( 551 MallocSizeOf aMallocSizeOf) const { 552 size_t n = mComplete.ShallowSizeOfExcludingThis(aMallocSizeOf); 553 for (const auto& data : mComplete.Values()) { 554 n += data.mResource->SizeOfIncludingThis(aMallocSizeOf); 555 } 556 557 return n; 558 } 559 560 template <typename Traits, typename Derived> 561 void SharedSubResourceCache<Traits, Derived>::LoadStarted( 562 const Key& aKey, LoadingValue& aValue) { 563 MOZ_DIAGNOSTIC_ASSERT(!aValue.IsLoading(), "Already loading? How?"); 564 MOZ_DIAGNOSTIC_ASSERT(KeyFromLoadingValue(aValue).KeyEquals(aKey)); 565 MOZ_DIAGNOSTIC_ASSERT(!mLoading.Contains(aKey), "Load not coalesced?"); 566 aValue.StartLoading(); 567 MOZ_ASSERT(aValue.IsLoading(), "Check that StartLoading is effectful."); 568 mLoading.InsertOrUpdate(aKey, &aValue); 569 } 570 571 template <typename Traits, typename Derived> 572 bool SharedSubResourceCache<Traits, Derived>::CompleteSubResource::Expired() 573 const { 574 return mExpirationTime.IsExpired(); 575 } 576 577 template <typename Traits, typename Derived> 578 void SharedSubResourceCache<Traits, Derived>::LoadCompleted( 579 LoadingValue& aValue) { 580 if (!aValue.IsLoading()) { 581 return; 582 } 583 auto key = KeyFromLoadingValue(aValue); 584 Maybe<LoadingValue*> value = mLoading.Extract(key); 585 MOZ_DIAGNOSTIC_ASSERT(value); 586 MOZ_DIAGNOSTIC_ASSERT(value.value() == &aValue); 587 (void)value; 588 aValue.SetLoadCompleted(); 589 MOZ_ASSERT(!aValue.IsLoading(), "Check that SetLoadCompleted is effectful."); 590 } 591 592 } // namespace mozilla 593 594 #endif