CanvasImageCache.cpp (12359B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "CanvasImageCache.h" 7 8 #include "gfx2DGlue.h" 9 #include "imgIRequest.h" 10 #include "mozilla/Preferences.h" 11 #include "mozilla/UniquePtr.h" 12 #include "mozilla/dom/Element.h" 13 #include "mozilla/gfx/2D.h" 14 #include "nsContentUtils.h" 15 #include "nsExpirationTracker.h" 16 #include "nsIImageLoadingContent.h" 17 #include "nsTHashtable.h" 18 19 namespace mozilla { 20 21 using namespace dom; 22 using namespace gfx; 23 24 /** 25 * Used for images specific to this one canvas. Required 26 * due to CORS security. 27 */ 28 struct ImageCacheKey { 29 ImageCacheKey(imgIContainer* aImage, CanvasRenderingContext2D* aContext, 30 BackendType aBackendType) 31 : mImage(aImage), mContext(aContext), mBackendType(aBackendType) {} 32 nsCOMPtr<imgIContainer> mImage; 33 CanvasRenderingContext2D* mContext; 34 BackendType mBackendType; 35 }; 36 37 /** 38 * Cache data needs to be separate from the entry 39 * for nsExpirationTracker. 40 */ 41 struct ImageCacheEntryData { 42 ImageCacheEntryData(const ImageCacheEntryData& aOther) 43 : mImage(aOther.mImage), 44 mContext(aOther.mContext), 45 mBackendType(aOther.mBackendType), 46 mSourceSurface(aOther.mSourceSurface), 47 mSize(aOther.mSize), 48 mIntrinsicSize(aOther.mIntrinsicSize), 49 mCropRect(aOther.mCropRect) {} 50 explicit ImageCacheEntryData(const ImageCacheKey& aKey) 51 : mImage(aKey.mImage), 52 mContext(aKey.mContext), 53 mBackendType(aKey.mBackendType) {} 54 55 nsExpirationState* GetExpirationState() { return &mState; } 56 size_t SizeInBytes() { return mSize.width * mSize.height * 4; } 57 58 // Key 59 nsCOMPtr<imgIContainer> mImage; 60 CanvasRenderingContext2D* mContext; 61 BackendType mBackendType; 62 // Value 63 RefPtr<SourceSurface> mSourceSurface; 64 IntSize mSize; 65 IntSize mIntrinsicSize; 66 Maybe<IntRect> mCropRect; 67 nsExpirationState mState; 68 }; 69 70 class ImageCacheEntry : public PLDHashEntryHdr { 71 public: 72 using KeyType = ImageCacheKey; 73 using KeyTypePointer = const ImageCacheKey*; 74 75 explicit ImageCacheEntry(const KeyType* aKey) 76 : mData(new ImageCacheEntryData(*aKey)) {} 77 ImageCacheEntry(const ImageCacheEntry& toCopy) 78 : mData(new ImageCacheEntryData(*toCopy.mData)) {} 79 ~ImageCacheEntry() = default; 80 81 bool KeyEquals(KeyTypePointer key) const { 82 return mData->mImage == key->mImage && mData->mContext == key->mContext && 83 mData->mBackendType == key->mBackendType; 84 } 85 86 static KeyTypePointer KeyToPointer(KeyType& key) { return &key; } 87 static PLDHashNumber HashKey(KeyTypePointer key) { 88 return HashGeneric(key->mImage.get(), key->mContext, key->mBackendType); 89 } 90 enum { ALLOW_MEMMOVE = true }; 91 92 UniquePtr<ImageCacheEntryData> mData; 93 }; 94 95 /** 96 * Used for all images across all canvases. 97 */ 98 struct AllCanvasImageCacheKey { 99 explicit AllCanvasImageCacheKey(imgIContainer* aImage, 100 BackendType aBackendType) 101 : mImage(aImage), mBackendType(aBackendType) {} 102 103 nsCOMPtr<imgIContainer> mImage; 104 BackendType mBackendType; 105 }; 106 107 class AllCanvasImageCacheEntry : public PLDHashEntryHdr { 108 public: 109 using KeyType = AllCanvasImageCacheKey; 110 using KeyTypePointer = const AllCanvasImageCacheKey*; 111 112 explicit AllCanvasImageCacheEntry(const KeyType* aKey) 113 : mImage(aKey->mImage), mBackendType(aKey->mBackendType) {} 114 115 AllCanvasImageCacheEntry(const AllCanvasImageCacheEntry& toCopy) 116 : mImage(toCopy.mImage), 117 mSourceSurface(toCopy.mSourceSurface), 118 mBackendType(toCopy.mBackendType) {} 119 120 ~AllCanvasImageCacheEntry() = default; 121 122 bool KeyEquals(KeyTypePointer key) const { 123 return mImage == key->mImage && mBackendType == key->mBackendType; 124 } 125 126 static KeyTypePointer KeyToPointer(KeyType& key) { return &key; } 127 static PLDHashNumber HashKey(KeyTypePointer key) { 128 return HashGeneric(key->mImage.get(), key->mBackendType); 129 } 130 enum { ALLOW_MEMMOVE = true }; 131 132 nsCOMPtr<imgIContainer> mImage; 133 RefPtr<SourceSurface> mSourceSurface; 134 BackendType mBackendType; 135 }; 136 137 class ImageCacheObserver; 138 139 class ImageCache final : public nsExpirationTracker<ImageCacheEntryData, 4> { 140 public: 141 // We use 3 generations of 1 second each to get a 2-3 seconds timeout. 142 enum { GENERATION_MS = 1000 }; 143 ImageCache(); 144 ~ImageCache(); 145 146 virtual void NotifyExpired(ImageCacheEntryData* aObject) override { 147 RemoveObject(aObject); 148 149 // Remove from the all canvas cache entry first since nsExpirationTracker 150 // will delete aObject. 151 mAllCanvasCache.RemoveEntry( 152 AllCanvasImageCacheKey(aObject->mImage, aObject->mBackendType)); 153 154 // Deleting the entry will delete aObject since the entry owns aObject. 155 mCache.RemoveEntry(ImageCacheKey(aObject->mImage, aObject->mContext, 156 aObject->mBackendType)); 157 } 158 159 nsTHashtable<ImageCacheEntry> mCache; 160 nsTHashtable<AllCanvasImageCacheEntry> mAllCanvasCache; 161 RefPtr<ImageCacheObserver> mImageCacheObserver; 162 }; 163 164 static ImageCache* gImageCache = nullptr; 165 166 // Listen memory-pressure event for image cache purge. 167 class ImageCacheObserver final : public nsIObserver { 168 public: 169 NS_DECL_ISUPPORTS 170 171 explicit ImageCacheObserver(ImageCache* aImageCache) 172 : mImageCache(aImageCache) { 173 RegisterObserverEvents(); 174 } 175 176 void Destroy() { 177 UnregisterObserverEvents(); 178 mImageCache = nullptr; 179 } 180 181 NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, 182 const char16_t* aSomeData) override { 183 if (!mImageCache || (strcmp(aTopic, "memory-pressure") != 0 && 184 strcmp(aTopic, "canvas-device-reset") != 0)) { 185 return NS_OK; 186 } 187 188 mImageCache->AgeAllGenerations(); 189 return NS_OK; 190 } 191 192 private: 193 virtual ~ImageCacheObserver() = default; 194 195 void RegisterObserverEvents() { 196 nsCOMPtr<nsIObserverService> observerService = 197 mozilla::services::GetObserverService(); 198 199 MOZ_ASSERT(observerService); 200 201 if (observerService) { 202 observerService->AddObserver(this, "memory-pressure", false); 203 observerService->AddObserver(this, "canvas-device-reset", false); 204 } 205 } 206 207 void UnregisterObserverEvents() { 208 nsCOMPtr<nsIObserverService> observerService = 209 mozilla::services::GetObserverService(); 210 211 // Do not assert on observerService here. This might be triggered by 212 // the cycle collector at a late enough time, that XPCOM services are 213 // no longer available. See bug 1029504. 214 if (observerService) { 215 observerService->RemoveObserver(this, "memory-pressure"); 216 observerService->RemoveObserver(this, "canvas-device-reset"); 217 } 218 } 219 220 ImageCache* mImageCache; 221 }; 222 223 NS_IMPL_ISUPPORTS(ImageCacheObserver, nsIObserver) 224 225 class CanvasImageCacheShutdownObserver final : public nsIObserver { 226 ~CanvasImageCacheShutdownObserver() = default; 227 228 public: 229 NS_DECL_ISUPPORTS 230 NS_DECL_NSIOBSERVER 231 }; 232 233 ImageCache::ImageCache() 234 : nsExpirationTracker<ImageCacheEntryData, 4>(GENERATION_MS, 235 "ImageCache"_ns) { 236 mImageCacheObserver = new ImageCacheObserver(this); 237 MOZ_RELEASE_ASSERT(mImageCacheObserver, 238 "GFX: Can't alloc ImageCacheObserver"); 239 } 240 241 ImageCache::~ImageCache() { 242 AgeAllGenerations(); 243 mImageCacheObserver->Destroy(); 244 } 245 246 static already_AddRefed<imgIContainer> GetImageContainer(dom::Element* aImage) { 247 nsCOMPtr<imgIRequest> request; 248 nsCOMPtr<nsIImageLoadingContent> ilc = do_QueryInterface(aImage); 249 if (!ilc) { 250 return nullptr; 251 } 252 253 ilc->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, 254 getter_AddRefs(request)); 255 if (!request) { 256 return nullptr; 257 } 258 259 nsCOMPtr<imgIContainer> imgContainer; 260 request->GetImage(getter_AddRefs(imgContainer)); 261 if (!imgContainer) { 262 return nullptr; 263 } 264 265 return imgContainer.forget(); 266 } 267 268 void CanvasImageCache::NotifyCanvasDestroyed( 269 CanvasRenderingContext2D* aContext) { 270 MOZ_ASSERT(aContext); 271 272 if (!NS_IsMainThread() || !gImageCache) { 273 return; 274 } 275 276 for (auto i = gImageCache->mCache.Iter(); !i.Done(); i.Next()) { 277 ImageCacheEntryData* data = i.Get()->mData.get(); 278 if (data->mContext == aContext) { 279 gImageCache->RemoveObject(data); 280 gImageCache->mAllCanvasCache.RemoveEntry( 281 AllCanvasImageCacheKey(data->mImage, data->mBackendType)); 282 i.Remove(); 283 } 284 } 285 } 286 287 void CanvasImageCache::NotifyDrawImage( 288 Element* aImage, CanvasRenderingContext2D* aContext, DrawTarget* aTarget, 289 SourceSurface* aSource, const IntSize& aSize, const IntSize& aIntrinsicSize, 290 const Maybe<IntRect>& aCropRect) { 291 MOZ_ASSERT(NS_IsMainThread()); 292 MOZ_ASSERT(aContext); 293 294 if (!aTarget || !aContext) { 295 return; 296 } 297 298 if (!gImageCache) { 299 gImageCache = new ImageCache(); 300 nsContentUtils::RegisterShutdownObserver( 301 new CanvasImageCacheShutdownObserver()); 302 } 303 304 nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage); 305 if (!imgContainer) { 306 return; 307 } 308 309 BackendType backendType = aTarget->GetBackendType(); 310 AllCanvasImageCacheKey allCanvasCacheKey(imgContainer, backendType); 311 ImageCacheKey canvasCacheKey(imgContainer, aContext, backendType); 312 ImageCacheEntry* entry = gImageCache->mCache.PutEntry(canvasCacheKey); 313 314 if (entry) { 315 if (entry->mData->mSourceSurface) { 316 // We are overwriting an existing entry. 317 gImageCache->RemoveObject(entry->mData.get()); 318 gImageCache->mAllCanvasCache.RemoveEntry(allCanvasCacheKey); 319 } 320 321 gImageCache->AddObject(entry->mData.get()); 322 entry->mData->mSourceSurface = aSource; 323 entry->mData->mSize = aSize; 324 entry->mData->mIntrinsicSize = aIntrinsicSize; 325 entry->mData->mCropRect = aCropRect; 326 327 AllCanvasImageCacheEntry* allEntry = 328 gImageCache->mAllCanvasCache.PutEntry(allCanvasCacheKey); 329 if (allEntry) { 330 allEntry->mSourceSurface = aSource; 331 } 332 } 333 } 334 335 SourceSurface* CanvasImageCache::LookupAllCanvas(Element* aImage, 336 DrawTarget* aTarget) { 337 MOZ_ASSERT(NS_IsMainThread()); 338 339 if (!gImageCache || !aTarget) { 340 return nullptr; 341 } 342 343 nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage); 344 if (!imgContainer) { 345 return nullptr; 346 } 347 348 AllCanvasImageCacheEntry* entry = gImageCache->mAllCanvasCache.GetEntry( 349 AllCanvasImageCacheKey(imgContainer, aTarget->GetBackendType())); 350 if (!entry) { 351 return nullptr; 352 } 353 354 return entry->mSourceSurface; 355 } 356 357 SourceSurface* CanvasImageCache::LookupCanvas( 358 Element* aImage, CanvasRenderingContext2D* aContext, DrawTarget* aTarget, 359 IntSize* aSizeOut, IntSize* aIntrinsicSizeOut, 360 Maybe<IntRect>* aCropRectOut) { 361 MOZ_ASSERT(NS_IsMainThread()); 362 MOZ_ASSERT(aContext); 363 364 if (!gImageCache || !aTarget) { 365 return nullptr; 366 } 367 368 nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage); 369 if (!imgContainer) { 370 return nullptr; 371 } 372 373 // We filter based on the backend type because we don't want a surface that 374 // was optimized for one backend to be preferred for another backend, as this 375 // could have performance implications. For example, a Skia optimized surface 376 // given to a D2D backend would cause an upload each time, and similarly a D2D 377 // optimized surface given to a Skia backend would cause a readback. For 378 // details, see bug 1794442. 379 ImageCacheEntry* entry = gImageCache->mCache.GetEntry( 380 ImageCacheKey(imgContainer, aContext, aTarget->GetBackendType())); 381 if (!entry) { 382 return nullptr; 383 } 384 385 MOZ_ASSERT(aSizeOut); 386 387 gImageCache->MarkUsed(entry->mData.get()); 388 *aSizeOut = entry->mData->mSize; 389 *aIntrinsicSizeOut = entry->mData->mIntrinsicSize; 390 *aCropRectOut = entry->mData->mCropRect; 391 return entry->mData->mSourceSurface; 392 } 393 394 NS_IMPL_ISUPPORTS(CanvasImageCacheShutdownObserver, nsIObserver) 395 396 NS_IMETHODIMP 397 CanvasImageCacheShutdownObserver::Observe(nsISupports* aSubject, 398 const char* aTopic, 399 const char16_t* aData) { 400 if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { 401 delete gImageCache; 402 gImageCache = nullptr; 403 404 nsContentUtils::UnregisterShutdownObserver(this); 405 } 406 407 return NS_OK; 408 } 409 410 } // namespace mozilla