|
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 /** |
|
7 * SurfaceCache is a service for caching temporary surfaces in imagelib. |
|
8 */ |
|
9 |
|
10 #include "SurfaceCache.h" |
|
11 |
|
12 #include <algorithm> |
|
13 #include "mozilla/Attributes.h" // for MOZ_THIS_IN_INITIALIZER_LIST |
|
14 #include "mozilla/DebugOnly.h" |
|
15 #include "mozilla/Preferences.h" |
|
16 #include "mozilla/RefPtr.h" |
|
17 #include "mozilla/StaticPtr.h" |
|
18 #include "nsIMemoryReporter.h" |
|
19 #include "gfx2DGlue.h" |
|
20 #include "gfxASurface.h" |
|
21 #include "gfxPattern.h" // Workaround for flaw in bug 921753 part 2. |
|
22 #include "gfxDrawable.h" |
|
23 #include "gfxPlatform.h" |
|
24 #include "nsAutoPtr.h" |
|
25 #include "nsExpirationTracker.h" |
|
26 #include "nsHashKeys.h" |
|
27 #include "nsRefPtrHashtable.h" |
|
28 #include "nsSize.h" |
|
29 #include "nsTArray.h" |
|
30 #include "prsystem.h" |
|
31 #include "SVGImageContext.h" |
|
32 |
|
33 using std::max; |
|
34 using std::min; |
|
35 using namespace mozilla::gfx; |
|
36 |
|
37 namespace mozilla { |
|
38 namespace image { |
|
39 |
|
40 class CachedSurface; |
|
41 class SurfaceCacheImpl; |
|
42 |
|
43 /////////////////////////////////////////////////////////////////////////////// |
|
44 // Static Data |
|
45 /////////////////////////////////////////////////////////////////////////////// |
|
46 |
|
47 // The single surface cache instance. |
|
48 static StaticRefPtr<SurfaceCacheImpl> sInstance; |
|
49 |
|
50 |
|
51 /////////////////////////////////////////////////////////////////////////////// |
|
52 // SurfaceCache Implementation |
|
53 /////////////////////////////////////////////////////////////////////////////// |
|
54 |
|
55 /* |
|
56 * Cost models the cost of storing a surface in the cache. Right now, this is |
|
57 * simply an estimate of the size of the surface in bytes, but in the future it |
|
58 * may be worth taking into account the cost of rematerializing the surface as |
|
59 * well. |
|
60 */ |
|
61 typedef size_t Cost; |
|
62 |
|
63 static Cost ComputeCost(const IntSize& aSize) |
|
64 { |
|
65 return aSize.width * aSize.height * 4; // width * height * 4 bytes (32bpp) |
|
66 } |
|
67 |
|
68 /* |
|
69 * Since we want to be able to make eviction decisions based on cost, we need to |
|
70 * be able to look up the CachedSurface which has a certain cost as well as the |
|
71 * cost associated with a certain CachedSurface. To make this possible, in data |
|
72 * structures we actually store a CostEntry, which contains a weak pointer to |
|
73 * its associated surface. |
|
74 * |
|
75 * To make usage of the weak pointer safe, SurfaceCacheImpl always calls |
|
76 * StartTracking after a surface is stored in the cache and StopTracking before |
|
77 * it is removed. |
|
78 */ |
|
79 class CostEntry |
|
80 { |
|
81 public: |
|
82 CostEntry(CachedSurface* aSurface, Cost aCost) |
|
83 : mSurface(aSurface) |
|
84 , mCost(aCost) |
|
85 { |
|
86 MOZ_ASSERT(aSurface, "Must have a surface"); |
|
87 } |
|
88 |
|
89 CachedSurface* GetSurface() const { return mSurface; } |
|
90 Cost GetCost() const { return mCost; } |
|
91 |
|
92 bool operator==(const CostEntry& aOther) const |
|
93 { |
|
94 return mSurface == aOther.mSurface && |
|
95 mCost == aOther.mCost; |
|
96 } |
|
97 |
|
98 bool operator<(const CostEntry& aOther) const |
|
99 { |
|
100 return mCost < aOther.mCost || |
|
101 (mCost == aOther.mCost && mSurface < aOther.mSurface); |
|
102 } |
|
103 |
|
104 private: |
|
105 CachedSurface* mSurface; |
|
106 Cost mCost; |
|
107 }; |
|
108 |
|
109 /* |
|
110 * A CachedSurface associates a surface with a key that uniquely identifies that |
|
111 * surface. |
|
112 */ |
|
113 class CachedSurface |
|
114 { |
|
115 public: |
|
116 NS_INLINE_DECL_REFCOUNTING(CachedSurface) |
|
117 |
|
118 CachedSurface(DrawTarget* aTarget, |
|
119 const IntSize aTargetSize, |
|
120 const Cost aCost, |
|
121 const ImageKey aImageKey, |
|
122 const SurfaceKey& aSurfaceKey) |
|
123 : mTarget(aTarget) |
|
124 , mTargetSize(aTargetSize) |
|
125 , mCost(aCost) |
|
126 , mImageKey(aImageKey) |
|
127 , mSurfaceKey(aSurfaceKey) |
|
128 { |
|
129 MOZ_ASSERT(mTarget, "Must have a valid DrawTarget"); |
|
130 MOZ_ASSERT(mImageKey, "Must have a valid image key"); |
|
131 } |
|
132 |
|
133 already_AddRefed<gfxDrawable> Drawable() const |
|
134 { |
|
135 nsRefPtr<gfxDrawable> drawable = |
|
136 new gfxSurfaceDrawable(mTarget, ThebesIntSize(mTargetSize)); |
|
137 return drawable.forget(); |
|
138 } |
|
139 |
|
140 ImageKey GetImageKey() const { return mImageKey; } |
|
141 SurfaceKey GetSurfaceKey() const { return mSurfaceKey; } |
|
142 CostEntry GetCostEntry() { return image::CostEntry(this, mCost); } |
|
143 nsExpirationState* GetExpirationState() { return &mExpirationState; } |
|
144 |
|
145 private: |
|
146 nsExpirationState mExpirationState; |
|
147 nsRefPtr<DrawTarget> mTarget; |
|
148 const IntSize mTargetSize; |
|
149 const Cost mCost; |
|
150 const ImageKey mImageKey; |
|
151 const SurfaceKey mSurfaceKey; |
|
152 }; |
|
153 |
|
154 /* |
|
155 * An ImageSurfaceCache is a per-image surface cache. For correctness we must be |
|
156 * able to remove all surfaces associated with an image when the image is |
|
157 * destroyed or invalidated. Since this will happen frequently, it makes sense |
|
158 * to make it cheap by storing the surfaces for each image separately. |
|
159 */ |
|
160 class ImageSurfaceCache |
|
161 { |
|
162 public: |
|
163 NS_INLINE_DECL_REFCOUNTING(ImageSurfaceCache) |
|
164 |
|
165 typedef nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface> SurfaceTable; |
|
166 |
|
167 bool IsEmpty() const { return mSurfaces.Count() == 0; } |
|
168 |
|
169 void Insert(const SurfaceKey& aKey, CachedSurface* aSurface) |
|
170 { |
|
171 MOZ_ASSERT(aSurface, "Should have a surface"); |
|
172 mSurfaces.Put(aKey, aSurface); |
|
173 } |
|
174 |
|
175 void Remove(CachedSurface* aSurface) |
|
176 { |
|
177 MOZ_ASSERT(aSurface, "Should have a surface"); |
|
178 MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()), |
|
179 "Should not be removing a surface we don't have"); |
|
180 |
|
181 mSurfaces.Remove(aSurface->GetSurfaceKey()); |
|
182 } |
|
183 |
|
184 already_AddRefed<CachedSurface> Lookup(const SurfaceKey& aSurfaceKey) |
|
185 { |
|
186 nsRefPtr<CachedSurface> surface; |
|
187 mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface)); |
|
188 return surface.forget(); |
|
189 } |
|
190 |
|
191 void ForEach(SurfaceTable::EnumReadFunction aFunction, void* aData) |
|
192 { |
|
193 mSurfaces.EnumerateRead(aFunction, aData); |
|
194 } |
|
195 |
|
196 private: |
|
197 SurfaceTable mSurfaces; |
|
198 }; |
|
199 |
|
200 /* |
|
201 * SurfaceCacheImpl is responsible for determining which surfaces will be cached |
|
202 * and managing the surface cache data structures. Rather than interact with |
|
203 * SurfaceCacheImpl directly, client code interacts with SurfaceCache, which |
|
204 * maintains high-level invariants and encapsulates the details of the surface |
|
205 * cache's implementation. |
|
206 */ |
|
207 class SurfaceCacheImpl : public nsIMemoryReporter |
|
208 { |
|
209 public: |
|
210 NS_DECL_ISUPPORTS |
|
211 |
|
212 SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS, |
|
213 uint32_t aSurfaceCacheSize) |
|
214 : mExpirationTracker(MOZ_THIS_IN_INITIALIZER_LIST(), |
|
215 aSurfaceCacheExpirationTimeMS) |
|
216 , mMemoryPressureObserver(new MemoryPressureObserver) |
|
217 , mMaxCost(aSurfaceCacheSize) |
|
218 , mAvailableCost(aSurfaceCacheSize) |
|
219 { |
|
220 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); |
|
221 if (os) |
|
222 os->AddObserver(mMemoryPressureObserver, "memory-pressure", false); |
|
223 } |
|
224 |
|
225 virtual ~SurfaceCacheImpl() |
|
226 { |
|
227 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); |
|
228 if (os) |
|
229 os->RemoveObserver(mMemoryPressureObserver, "memory-pressure"); |
|
230 |
|
231 UnregisterWeakMemoryReporter(this); |
|
232 } |
|
233 |
|
234 void InitMemoryReporter() { |
|
235 RegisterWeakMemoryReporter(this); |
|
236 } |
|
237 |
|
238 void Insert(DrawTarget* aTarget, |
|
239 IntSize aTargetSize, |
|
240 const Cost aCost, |
|
241 const ImageKey aImageKey, |
|
242 const SurfaceKey& aSurfaceKey) |
|
243 { |
|
244 MOZ_ASSERT(!Lookup(aImageKey, aSurfaceKey).take(), |
|
245 "Inserting a duplicate drawable into the SurfaceCache"); |
|
246 |
|
247 // If this is bigger than the maximum cache size, refuse to cache it. |
|
248 if (!CanHold(aCost)) |
|
249 return; |
|
250 |
|
251 nsRefPtr<CachedSurface> surface = |
|
252 new CachedSurface(aTarget, aTargetSize, aCost, aImageKey, aSurfaceKey); |
|
253 |
|
254 // Remove elements in order of cost until we can fit this in the cache. |
|
255 while (aCost > mAvailableCost) { |
|
256 MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and it still won't fit"); |
|
257 Remove(mCosts.LastElement().GetSurface()); |
|
258 } |
|
259 |
|
260 // Locate the appropriate per-image cache. If there's not an existing cache |
|
261 // for this image, create it. |
|
262 nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey); |
|
263 if (!cache) { |
|
264 cache = new ImageSurfaceCache; |
|
265 mImageCaches.Put(aImageKey, cache); |
|
266 } |
|
267 |
|
268 // Insert. |
|
269 MOZ_ASSERT(aCost <= mAvailableCost, "Inserting despite too large a cost"); |
|
270 cache->Insert(aSurfaceKey, surface); |
|
271 StartTracking(surface); |
|
272 } |
|
273 |
|
274 void Remove(CachedSurface* aSurface) |
|
275 { |
|
276 MOZ_ASSERT(aSurface, "Should have a surface"); |
|
277 const ImageKey imageKey = aSurface->GetImageKey(); |
|
278 |
|
279 nsRefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey); |
|
280 MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache"); |
|
281 |
|
282 StopTracking(aSurface); |
|
283 cache->Remove(aSurface); |
|
284 |
|
285 // Remove the per-image cache if it's unneeded now. |
|
286 if (cache->IsEmpty()) { |
|
287 mImageCaches.Remove(imageKey); |
|
288 } |
|
289 } |
|
290 |
|
291 void StartTracking(CachedSurface* aSurface) |
|
292 { |
|
293 CostEntry costEntry = aSurface->GetCostEntry(); |
|
294 MOZ_ASSERT(costEntry.GetCost() <= mAvailableCost, |
|
295 "Cost too large and the caller didn't catch it"); |
|
296 |
|
297 mAvailableCost -= costEntry.GetCost(); |
|
298 mCosts.InsertElementSorted(costEntry); |
|
299 mExpirationTracker.AddObject(aSurface); |
|
300 } |
|
301 |
|
302 void StopTracking(CachedSurface* aSurface) |
|
303 { |
|
304 MOZ_ASSERT(aSurface, "Should have a surface"); |
|
305 CostEntry costEntry = aSurface->GetCostEntry(); |
|
306 |
|
307 mExpirationTracker.RemoveObject(aSurface); |
|
308 DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry); |
|
309 mAvailableCost += costEntry.GetCost(); |
|
310 |
|
311 MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface"); |
|
312 MOZ_ASSERT(mAvailableCost <= mMaxCost, "More available cost than we started with"); |
|
313 } |
|
314 |
|
315 already_AddRefed<gfxDrawable> Lookup(const ImageKey aImageKey, |
|
316 const SurfaceKey& aSurfaceKey) |
|
317 { |
|
318 nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey); |
|
319 if (!cache) |
|
320 return nullptr; // No cached surfaces for this image. |
|
321 |
|
322 nsRefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey); |
|
323 if (!surface) |
|
324 return nullptr; // Lookup in the per-image cache missed. |
|
325 |
|
326 mExpirationTracker.MarkUsed(surface); |
|
327 return surface->Drawable(); |
|
328 } |
|
329 |
|
330 bool CanHold(const Cost aCost) const |
|
331 { |
|
332 return aCost <= mMaxCost; |
|
333 } |
|
334 |
|
335 void Discard(const ImageKey aImageKey) |
|
336 { |
|
337 nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey); |
|
338 if (!cache) |
|
339 return; // No cached surfaces for this image, so nothing to do. |
|
340 |
|
341 // Discard all of the cached surfaces for this image. |
|
342 // XXX(seth): This is O(n^2) since for each item in the cache we are |
|
343 // removing an element from the costs array. Since n is expected to be |
|
344 // small, performance should be good, but if usage patterns change we should |
|
345 // change the data structure used for mCosts. |
|
346 cache->ForEach(DoStopTracking, this); |
|
347 |
|
348 // The per-image cache isn't needed anymore, so remove it as well. |
|
349 mImageCaches.Remove(aImageKey); |
|
350 } |
|
351 |
|
352 void DiscardAll() |
|
353 { |
|
354 // Remove in order of cost because mCosts is an array and the other data |
|
355 // structures are all hash tables. |
|
356 while (!mCosts.IsEmpty()) { |
|
357 Remove(mCosts.LastElement().GetSurface()); |
|
358 } |
|
359 } |
|
360 |
|
361 static PLDHashOperator DoStopTracking(const SurfaceKey&, |
|
362 CachedSurface* aSurface, |
|
363 void* aCache) |
|
364 { |
|
365 static_cast<SurfaceCacheImpl*>(aCache)->StopTracking(aSurface); |
|
366 return PL_DHASH_NEXT; |
|
367 } |
|
368 |
|
369 NS_IMETHOD |
|
370 CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData) |
|
371 { |
|
372 return MOZ_COLLECT_REPORT( |
|
373 "imagelib-surface-cache", KIND_OTHER, UNITS_BYTES, |
|
374 SizeOfSurfacesEstimate(), |
|
375 "Memory used by the imagelib temporary surface cache."); |
|
376 } |
|
377 |
|
378 // XXX(seth): This is currently only an estimate and, since we don't know |
|
379 // which surfaces are in GPU memory and which aren't, it's reported as |
|
380 // KIND_OTHER and will also show up in heap-unclassified. Bug 923302 will |
|
381 // make this nicer. |
|
382 Cost SizeOfSurfacesEstimate() const |
|
383 { |
|
384 return mMaxCost - mAvailableCost; |
|
385 } |
|
386 |
|
387 private: |
|
388 already_AddRefed<ImageSurfaceCache> GetImageCache(const ImageKey aImageKey) |
|
389 { |
|
390 nsRefPtr<ImageSurfaceCache> imageCache; |
|
391 mImageCaches.Get(aImageKey, getter_AddRefs(imageCache)); |
|
392 return imageCache.forget(); |
|
393 } |
|
394 |
|
395 struct SurfaceTracker : public nsExpirationTracker<CachedSurface, 2> |
|
396 { |
|
397 SurfaceTracker(SurfaceCacheImpl* aCache, uint32_t aSurfaceCacheExpirationTimeMS) |
|
398 : nsExpirationTracker<CachedSurface, 2>(aSurfaceCacheExpirationTimeMS) |
|
399 , mCache(aCache) |
|
400 { } |
|
401 |
|
402 protected: |
|
403 virtual void NotifyExpired(CachedSurface* aSurface) MOZ_OVERRIDE |
|
404 { |
|
405 if (mCache) { |
|
406 mCache->Remove(aSurface); |
|
407 } |
|
408 } |
|
409 |
|
410 private: |
|
411 SurfaceCacheImpl* const mCache; // Weak pointer to owner. |
|
412 }; |
|
413 |
|
414 struct MemoryPressureObserver : public nsIObserver |
|
415 { |
|
416 NS_DECL_ISUPPORTS |
|
417 |
|
418 virtual ~MemoryPressureObserver() { } |
|
419 |
|
420 NS_IMETHOD Observe(nsISupports*, const char* aTopic, const char16_t*) |
|
421 { |
|
422 if (sInstance && strcmp(aTopic, "memory-pressure") == 0) { |
|
423 sInstance->DiscardAll(); |
|
424 } |
|
425 return NS_OK; |
|
426 } |
|
427 }; |
|
428 |
|
429 |
|
430 nsTArray<CostEntry> mCosts; |
|
431 nsRefPtrHashtable<nsPtrHashKey<Image>, ImageSurfaceCache> mImageCaches; |
|
432 SurfaceTracker mExpirationTracker; |
|
433 nsRefPtr<MemoryPressureObserver> mMemoryPressureObserver; |
|
434 const Cost mMaxCost; |
|
435 Cost mAvailableCost; |
|
436 }; |
|
437 |
|
438 NS_IMPL_ISUPPORTS(SurfaceCacheImpl, nsIMemoryReporter) |
|
439 NS_IMPL_ISUPPORTS(SurfaceCacheImpl::MemoryPressureObserver, nsIObserver) |
|
440 |
|
441 /////////////////////////////////////////////////////////////////////////////// |
|
442 // Public API |
|
443 /////////////////////////////////////////////////////////////////////////////// |
|
444 |
|
445 /* static */ void |
|
446 SurfaceCache::Initialize() |
|
447 { |
|
448 // Initialize preferences. |
|
449 MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once"); |
|
450 |
|
451 // Length of time before an unused surface is removed from the cache, in milliseconds. |
|
452 // The default value gives an expiration time of 1 minute. |
|
453 uint32_t surfaceCacheExpirationTimeMS = |
|
454 Preferences::GetUint("image.mem.surfacecache.min_expiration_ms", 60 * 1000); |
|
455 |
|
456 // Maximum size of the surface cache, in kilobytes. |
|
457 // The default is 100MB. (But we may override this for e.g. B2G.) |
|
458 uint32_t surfaceCacheMaxSizeKB = |
|
459 Preferences::GetUint("image.mem.surfacecache.max_size_kb", 100 * 1024); |
|
460 |
|
461 // A knob determining the actual size of the surface cache. Currently the |
|
462 // cache is (size of main memory) / (surface cache size factor) KB |
|
463 // or (surface cache max size) KB, whichever is smaller. The formula |
|
464 // may change in the future, though. |
|
465 // The default value is 64, which yields a 64MB cache on a 4GB machine. |
|
466 // The smallest machines we are likely to run this code on have 256MB |
|
467 // of memory, which would yield a 4MB cache on the default setting. |
|
468 uint32_t surfaceCacheSizeFactor = |
|
469 Preferences::GetUint("image.mem.surfacecache.size_factor", 64); |
|
470 |
|
471 // Clamp to avoid division by zero below. |
|
472 surfaceCacheSizeFactor = max(surfaceCacheSizeFactor, 1u); |
|
473 |
|
474 // Compute the size of the surface cache. |
|
475 uint32_t proposedSize = PR_GetPhysicalMemorySize() / surfaceCacheSizeFactor; |
|
476 uint32_t surfaceCacheSizeBytes = min(proposedSize, surfaceCacheMaxSizeKB * 1024); |
|
477 |
|
478 // Create the surface cache singleton with the requested expiration time and |
|
479 // size. Note that the size is a limit that the cache may not grow beyond, but |
|
480 // we do not actually allocate any storage for surfaces at this time. |
|
481 sInstance = new SurfaceCacheImpl(surfaceCacheExpirationTimeMS, |
|
482 surfaceCacheSizeBytes); |
|
483 sInstance->InitMemoryReporter(); |
|
484 } |
|
485 |
|
486 /* static */ void |
|
487 SurfaceCache::Shutdown() |
|
488 { |
|
489 MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?"); |
|
490 sInstance = nullptr; |
|
491 } |
|
492 |
|
493 /* static */ already_AddRefed<gfxDrawable> |
|
494 SurfaceCache::Lookup(const ImageKey aImageKey, |
|
495 const SurfaceKey& aSurfaceKey) |
|
496 { |
|
497 MOZ_ASSERT(sInstance, "Should be initialized"); |
|
498 MOZ_ASSERT(NS_IsMainThread()); |
|
499 |
|
500 return sInstance->Lookup(aImageKey, aSurfaceKey); |
|
501 } |
|
502 |
|
503 /* static */ void |
|
504 SurfaceCache::Insert(DrawTarget* aTarget, |
|
505 const ImageKey aImageKey, |
|
506 const SurfaceKey& aSurfaceKey) |
|
507 { |
|
508 MOZ_ASSERT(sInstance, "Should be initialized"); |
|
509 MOZ_ASSERT(NS_IsMainThread()); |
|
510 |
|
511 Cost cost = ComputeCost(aSurfaceKey.Size()); |
|
512 return sInstance->Insert(aTarget, aSurfaceKey.Size(), cost, aImageKey, |
|
513 aSurfaceKey); |
|
514 } |
|
515 |
|
516 /* static */ bool |
|
517 SurfaceCache::CanHold(const IntSize& aSize) |
|
518 { |
|
519 MOZ_ASSERT(sInstance, "Should be initialized"); |
|
520 MOZ_ASSERT(NS_IsMainThread()); |
|
521 |
|
522 Cost cost = ComputeCost(aSize); |
|
523 return sInstance->CanHold(cost); |
|
524 } |
|
525 |
|
526 /* static */ void |
|
527 SurfaceCache::Discard(Image* aImageKey) |
|
528 { |
|
529 MOZ_ASSERT(sInstance, "Should be initialized"); |
|
530 MOZ_ASSERT(NS_IsMainThread()); |
|
531 |
|
532 return sInstance->Discard(aImageKey); |
|
533 } |
|
534 |
|
535 /* static */ void |
|
536 SurfaceCache::DiscardAll() |
|
537 { |
|
538 MOZ_ASSERT(sInstance, "Should be initialized"); |
|
539 MOZ_ASSERT(NS_IsMainThread()); |
|
540 |
|
541 return sInstance->DiscardAll(); |
|
542 } |
|
543 |
|
544 } // namespace image |
|
545 } // namespace mozilla |