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