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 #include "nsAutoPtr.h"
8 #include "nsIImageLoadingContent.h"
9 #include "nsExpirationTracker.h"
10 #include "imgIRequest.h"
11 #include "mozilla/dom/Element.h"
12 #include "nsTHashtable.h"
13 #include "mozilla/dom/HTMLCanvasElement.h"
14 #include "nsContentUtils.h"
15 #include "mozilla/Preferences.h"
16 #include "mozilla/gfx/2D.h"
17 #include "gfx2DGlue.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 {
ImageCacheKeymozilla::ImageCacheKey30   ImageCacheKey(imgIContainer* aImage,
31                 HTMLCanvasElement* aCanvas,
32                 bool aIsAccelerated)
33     : mImage(aImage)
34     , mCanvas(aCanvas)
35     , mIsAccelerated(aIsAccelerated)
36   {}
37   nsCOMPtr<imgIContainer> mImage;
38   HTMLCanvasElement* mCanvas;
39   bool mIsAccelerated;
40 };
41 
42 /**
43  * Cache data needs to be separate from the entry
44  * for nsExpirationTracker.
45  */
46 struct ImageCacheEntryData
47 {
ImageCacheEntryDatamozilla::ImageCacheEntryData48   ImageCacheEntryData(const ImageCacheEntryData& aOther)
49     : mImage(aOther.mImage)
50     , mCanvas(aOther.mCanvas)
51     , mIsAccelerated(aOther.mIsAccelerated)
52     , mSourceSurface(aOther.mSourceSurface)
53     , mSize(aOther.mSize)
54   {}
ImageCacheEntryDatamozilla::ImageCacheEntryData55   explicit ImageCacheEntryData(const ImageCacheKey& aKey)
56     : mImage(aKey.mImage)
57     , mCanvas(aKey.mCanvas)
58     , mIsAccelerated(aKey.mIsAccelerated)
59   {}
60 
GetExpirationStatemozilla::ImageCacheEntryData61   nsExpirationState* GetExpirationState() { return &mState; }
SizeInBytesmozilla::ImageCacheEntryData62   size_t SizeInBytes() { return mSize.width * mSize.height * 4; }
63 
64   // Key
65   nsCOMPtr<imgIContainer> mImage;
66   HTMLCanvasElement* mCanvas;
67   bool mIsAccelerated;
68   // Value
69   RefPtr<SourceSurface> mSourceSurface;
70   IntSize mSize;
71   nsExpirationState mState;
72 };
73 
74 class ImageCacheEntry : public PLDHashEntryHdr
75 {
76 public:
77   typedef ImageCacheKey KeyType;
78   typedef const ImageCacheKey* KeyTypePointer;
79 
ImageCacheEntry(const KeyType * aKey)80   explicit ImageCacheEntry(const KeyType* aKey) :
81       mData(new ImageCacheEntryData(*aKey)) {}
ImageCacheEntry(const ImageCacheEntry & toCopy)82   ImageCacheEntry(const ImageCacheEntry& toCopy) :
83       mData(new ImageCacheEntryData(*toCopy.mData)) {}
~ImageCacheEntry()84   ~ImageCacheEntry() {}
85 
KeyEquals(KeyTypePointer key) const86   bool KeyEquals(KeyTypePointer key) const
87   {
88     return mData->mImage == key->mImage &&
89            mData->mCanvas == key->mCanvas &&
90            mData->mIsAccelerated == key->mIsAccelerated;
91   }
92 
KeyToPointer(KeyType & key)93   static KeyTypePointer KeyToPointer(KeyType& key) { return &key; }
HashKey(KeyTypePointer key)94   static PLDHashNumber HashKey(KeyTypePointer key)
95   {
96     return HashGeneric(key->mImage.get(), key->mCanvas, key->mIsAccelerated);
97   }
98   enum { ALLOW_MEMMOVE = true };
99 
100   nsAutoPtr<ImageCacheEntryData> mData;
101 };
102 
103 
104 /**
105  * Used for all images across all canvases.
106  */
107 struct AllCanvasImageCacheKey
108 {
AllCanvasImageCacheKeymozilla::AllCanvasImageCacheKey109   AllCanvasImageCacheKey(imgIContainer* aImage,
110                          bool aIsAccelerated)
111     : mImage(aImage)
112     , mIsAccelerated(aIsAccelerated)
113   {}
114 
115   nsCOMPtr<imgIContainer> mImage;
116   bool mIsAccelerated;
117 };
118 
119 class AllCanvasImageCacheEntry : public PLDHashEntryHdr {
120 public:
121   typedef AllCanvasImageCacheKey KeyType;
122   typedef const AllCanvasImageCacheKey* KeyTypePointer;
123 
AllCanvasImageCacheEntry(const KeyType * aKey)124   explicit AllCanvasImageCacheEntry(const KeyType* aKey)
125     : mImage(aKey->mImage)
126     , mIsAccelerated(aKey->mIsAccelerated)
127   {}
128 
AllCanvasImageCacheEntry(const AllCanvasImageCacheEntry & toCopy)129   AllCanvasImageCacheEntry(const AllCanvasImageCacheEntry &toCopy)
130     : mImage(toCopy.mImage)
131     , mIsAccelerated(toCopy.mIsAccelerated)
132     , mSourceSurface(toCopy.mSourceSurface)
133   {}
134 
~AllCanvasImageCacheEntry()135   ~AllCanvasImageCacheEntry() {}
136 
KeyEquals(KeyTypePointer key) const137   bool KeyEquals(KeyTypePointer key) const
138   {
139     return mImage == key->mImage &&
140            mIsAccelerated == key->mIsAccelerated;
141   }
142 
KeyToPointer(KeyType & key)143   static KeyTypePointer KeyToPointer(KeyType& key) { return &key; }
HashKey(KeyTypePointer key)144   static PLDHashNumber HashKey(KeyTypePointer key)
145   {
146     return HashGeneric(key->mImage.get(), key->mIsAccelerated);
147   }
148   enum { ALLOW_MEMMOVE = true };
149 
150   nsCOMPtr<imgIContainer> mImage;
151   bool mIsAccelerated;
152   RefPtr<SourceSurface> mSourceSurface;
153 };
154 
155 static bool sPrefsInitialized = false;
156 static int32_t sCanvasImageCacheLimit = 0;
157 
158 class ImageCacheObserver;
159 
160 class ImageCache final : public nsExpirationTracker<ImageCacheEntryData,4>
161 {
162 public:
163   // We use 3 generations of 1 second each to get a 2-3 seconds timeout.
164   enum { GENERATION_MS = 1000 };
165   ImageCache();
166   ~ImageCache();
167 
NotifyExpired(ImageCacheEntryData * aObject)168   virtual void NotifyExpired(ImageCacheEntryData* aObject)
169   {
170     mTotal -= aObject->SizeInBytes();
171     RemoveObject(aObject);
172 
173     // Remove from the all canvas cache entry first since nsExpirationTracker
174     // will delete aObject.
175     mAllCanvasCache.RemoveEntry(AllCanvasImageCacheKey(aObject->mImage, aObject->mIsAccelerated));
176 
177     // Deleting the entry will delete aObject since the entry owns aObject.
178     mCache.RemoveEntry(ImageCacheKey(aObject->mImage, aObject->mCanvas, aObject->mIsAccelerated));
179   }
180 
181   nsTHashtable<ImageCacheEntry> mCache;
182   nsTHashtable<AllCanvasImageCacheEntry> mAllCanvasCache;
183   size_t mTotal;
184   RefPtr<ImageCacheObserver> mImageCacheObserver;
185 };
186 
187 static ImageCache* gImageCache = nullptr;
188 
189 // Listen memory-pressure event for image cache purge.
190 class ImageCacheObserver final : public nsIObserver
191 {
192 public:
193   NS_DECL_ISUPPORTS
194 
ImageCacheObserver(ImageCache * aImageCache)195   explicit ImageCacheObserver(ImageCache* aImageCache)
196     : mImageCache(aImageCache)
197   {
198     RegisterMemoryPressureEvent();
199   }
200 
Destroy()201   void Destroy()
202   {
203     UnregisterMemoryPressureEvent();
204     mImageCache = nullptr;
205   }
206 
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aSomeData)207   NS_IMETHOD Observe(nsISupports* aSubject,
208                      const char* aTopic,
209                      const char16_t* aSomeData) override
210   {
211     if (!mImageCache || strcmp(aTopic, "memory-pressure")) {
212       return NS_OK;
213     }
214 
215     mImageCache->AgeAllGenerations();
216     return NS_OK;
217   }
218 
219 private:
~ImageCacheObserver()220   virtual ~ImageCacheObserver()
221   {
222   }
223 
RegisterMemoryPressureEvent()224   void RegisterMemoryPressureEvent()
225   {
226     nsCOMPtr<nsIObserverService> observerService =
227       mozilla::services::GetObserverService();
228 
229     MOZ_ASSERT(observerService);
230 
231     if (observerService) {
232       observerService->AddObserver(this, "memory-pressure", false);
233     }
234   }
235 
UnregisterMemoryPressureEvent()236   void UnregisterMemoryPressureEvent()
237   {
238     nsCOMPtr<nsIObserverService> observerService =
239         mozilla::services::GetObserverService();
240 
241     // Do not assert on observerService here. This might be triggered by
242     // the cycle collector at a late enough time, that XPCOM services are
243     // no longer available. See bug 1029504.
244     if (observerService) {
245         observerService->RemoveObserver(this, "memory-pressure");
246     }
247   }
248 
249   ImageCache* mImageCache;
250 };
251 
252 NS_IMPL_ISUPPORTS(ImageCacheObserver, nsIObserver)
253 
254 class CanvasImageCacheShutdownObserver final : public nsIObserver
255 {
~CanvasImageCacheShutdownObserver()256   ~CanvasImageCacheShutdownObserver() {}
257 public:
258   NS_DECL_ISUPPORTS
259   NS_DECL_NSIOBSERVER
260 };
261 
ImageCache()262 ImageCache::ImageCache()
263   : nsExpirationTracker<ImageCacheEntryData,4>(GENERATION_MS, "ImageCache")
264   , mTotal(0)
265 {
266   if (!sPrefsInitialized) {
267     sPrefsInitialized = true;
268     Preferences::AddIntVarCache(&sCanvasImageCacheLimit, "canvas.image.cache.limit", 0);
269   }
270   mImageCacheObserver = new ImageCacheObserver(this);
271   MOZ_RELEASE_ASSERT(mImageCacheObserver, "GFX: Can't alloc ImageCacheObserver");
272 }
273 
~ImageCache()274 ImageCache::~ImageCache() {
275   AgeAllGenerations();
276   mImageCacheObserver->Destroy();
277 }
278 
279 static already_AddRefed<imgIContainer>
GetImageContainer(dom::Element * aImage)280 GetImageContainer(dom::Element* aImage)
281 {
282   nsCOMPtr<imgIRequest> request;
283   nsCOMPtr<nsIImageLoadingContent> ilc = do_QueryInterface(aImage);
284   if (!ilc) {
285     return nullptr;
286   }
287 
288   ilc->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
289                   getter_AddRefs(request));
290   if (!request) {
291     return nullptr;
292   }
293 
294   nsCOMPtr<imgIContainer> imgContainer;
295   request->GetImage(getter_AddRefs(imgContainer));
296   if (!imgContainer) {
297     return nullptr;
298   }
299 
300   return imgContainer.forget();
301 }
302 
303 void
NotifyDrawImage(Element * aImage,HTMLCanvasElement * aCanvas,SourceSurface * aSource,const IntSize & aSize,bool aIsAccelerated)304 CanvasImageCache::NotifyDrawImage(Element* aImage,
305                                   HTMLCanvasElement* aCanvas,
306                                   SourceSurface* aSource,
307                                   const IntSize& aSize,
308                                   bool aIsAccelerated)
309 {
310   if (!gImageCache) {
311     gImageCache = new ImageCache();
312     nsContentUtils::RegisterShutdownObserver(new CanvasImageCacheShutdownObserver());
313   }
314 
315   nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage);
316   if (!imgContainer) {
317     return;
318   }
319 
320   AllCanvasImageCacheKey allCanvasCacheKey(imgContainer, aIsAccelerated);
321   ImageCacheKey canvasCacheKey(imgContainer, aCanvas, aIsAccelerated);
322   ImageCacheEntry* entry = gImageCache->mCache.PutEntry(canvasCacheKey);
323 
324   if (entry) {
325     if (entry->mData->mSourceSurface) {
326       // We are overwriting an existing entry.
327       gImageCache->mTotal -= entry->mData->SizeInBytes();
328       gImageCache->RemoveObject(entry->mData);
329       gImageCache->mAllCanvasCache.RemoveEntry(allCanvasCacheKey);
330     }
331 
332     gImageCache->AddObject(entry->mData);
333     entry->mData->mSourceSurface = aSource;
334     entry->mData->mSize = aSize;
335     gImageCache->mTotal += entry->mData->SizeInBytes();
336 
337     AllCanvasImageCacheEntry* allEntry = gImageCache->mAllCanvasCache.PutEntry(allCanvasCacheKey);
338     if (allEntry) {
339       allEntry->mSourceSurface = aSource;
340     }
341   }
342 
343   if (!sCanvasImageCacheLimit)
344     return;
345 
346   // Expire the image cache early if its larger than we want it to be.
347   while (gImageCache->mTotal > size_t(sCanvasImageCacheLimit))
348     gImageCache->AgeOneGeneration();
349 }
350 
351 SourceSurface*
LookupAllCanvas(Element * aImage,bool aIsAccelerated)352 CanvasImageCache::LookupAllCanvas(Element* aImage,
353                                   bool aIsAccelerated)
354 {
355   if (!gImageCache) {
356     return nullptr;
357   }
358 
359   nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage);
360   if (!imgContainer) {
361     return nullptr;
362   }
363 
364   AllCanvasImageCacheEntry* entry =
365     gImageCache->mAllCanvasCache.GetEntry(AllCanvasImageCacheKey(imgContainer, aIsAccelerated));
366   if (!entry) {
367     return nullptr;
368   }
369 
370   return entry->mSourceSurface;
371 }
372 
373 SourceSurface*
LookupCanvas(Element * aImage,HTMLCanvasElement * aCanvas,IntSize * aSizeOut,bool aIsAccelerated)374 CanvasImageCache::LookupCanvas(Element* aImage,
375                                HTMLCanvasElement* aCanvas,
376                                IntSize* aSizeOut,
377                                bool aIsAccelerated)
378 {
379   if (!gImageCache) {
380     return nullptr;
381   }
382 
383   nsCOMPtr<imgIContainer> imgContainer = GetImageContainer(aImage);
384   if (!imgContainer) {
385     return nullptr;
386   }
387 
388   ImageCacheEntry* entry =
389     gImageCache->mCache.GetEntry(ImageCacheKey(imgContainer, aCanvas, aIsAccelerated));
390   if (!entry) {
391     return nullptr;
392   }
393 
394   MOZ_ASSERT(aSizeOut);
395 
396   gImageCache->MarkUsed(entry->mData);
397   *aSizeOut = entry->mData->mSize;
398   return entry->mData->mSourceSurface;
399 }
400 
401 
NS_IMPL_ISUPPORTS(CanvasImageCacheShutdownObserver,nsIObserver)402 NS_IMPL_ISUPPORTS(CanvasImageCacheShutdownObserver, nsIObserver)
403 
404 NS_IMETHODIMP
405 CanvasImageCacheShutdownObserver::Observe(nsISupports *aSubject,
406                                           const char *aTopic,
407                                           const char16_t *aData)
408 {
409   if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
410     delete gImageCache;
411     gImageCache = nullptr;
412 
413     nsContentUtils::UnregisterShutdownObserver(this);
414   }
415 
416   return NS_OK;
417 }
418 
419 } // namespace mozilla
420