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 "gfxGradientCache.h"
7 
8 #include "MainThreadUtils.h"
9 #include "mozilla/gfx/2D.h"
10 #include "mozilla/DataMutex.h"
11 #include "nsTArray.h"
12 #include "PLDHashTable.h"
13 #include "nsExpirationTracker.h"
14 #include "nsClassHashtable.h"
15 #include <time.h>
16 
17 namespace mozilla {
18 namespace gfx {
19 
20 using namespace mozilla;
21 
22 struct GradientCacheKey : public PLDHashEntryHdr {
23   typedef const GradientCacheKey& KeyType;
24   typedef const GradientCacheKey* KeyTypePointer;
25   enum { ALLOW_MEMMOVE = true };
26   const CopyableTArray<GradientStop> mStops;
27   ExtendMode mExtend;
28   BackendType mBackendType;
29 
GradientCacheKeymozilla::gfx::GradientCacheKey30   GradientCacheKey(const nsTArray<GradientStop>& aStops, ExtendMode aExtend,
31                    BackendType aBackendType)
32       : mStops(aStops), mExtend(aExtend), mBackendType(aBackendType) {}
33 
GradientCacheKeymozilla::gfx::GradientCacheKey34   explicit GradientCacheKey(const GradientCacheKey* aOther)
35       : mStops(aOther->mStops),
36         mExtend(aOther->mExtend),
37         mBackendType(aOther->mBackendType) {}
38 
39   GradientCacheKey(GradientCacheKey&& aOther) = default;
40 
41   union FloatUint32 {
42     float f;
43     uint32_t u;
44   };
45 
HashKeymozilla::gfx::GradientCacheKey46   static PLDHashNumber HashKey(const KeyTypePointer aKey) {
47     PLDHashNumber hash = 0;
48     FloatUint32 convert;
49     hash = AddToHash(hash, int(aKey->mBackendType));
50     hash = AddToHash(hash, int(aKey->mExtend));
51     for (uint32_t i = 0; i < aKey->mStops.Length(); i++) {
52       hash = AddToHash(hash, aKey->mStops[i].color.ToABGR());
53       // Use the float bits as hash, except for the cases of 0.0 and -0.0 which
54       // both map to 0
55       convert.f = aKey->mStops[i].offset;
56       hash = AddToHash(hash, convert.f ? convert.u : 0);
57     }
58     return hash;
59   }
60 
KeyEqualsmozilla::gfx::GradientCacheKey61   bool KeyEquals(KeyTypePointer aKey) const {
62     bool sameStops = true;
63     if (aKey->mStops.Length() != mStops.Length()) {
64       sameStops = false;
65     } else {
66       for (uint32_t i = 0; i < mStops.Length(); i++) {
67         if (mStops[i].color.ToABGR() != aKey->mStops[i].color.ToABGR() ||
68             mStops[i].offset != aKey->mStops[i].offset) {
69           sameStops = false;
70           break;
71         }
72       }
73     }
74 
75     return sameStops && (aKey->mBackendType == mBackendType) &&
76            (aKey->mExtend == mExtend);
77   }
KeyToPointermozilla::gfx::GradientCacheKey78   static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
79 };
80 
81 /**
82  * This class is what is cached. It need to be allocated in an object separated
83  * to the cache entry to be able to be tracked by the nsExpirationTracker.
84  * */
85 struct GradientCacheData {
GradientCacheDatamozilla::gfx::GradientCacheData86   GradientCacheData(GradientStops* aStops, GradientCacheKey&& aKey)
87       : mStops(aStops), mKey(std::move(aKey)) {}
88 
89   GradientCacheData(GradientCacheData&& aOther) = default;
90 
GetExpirationStatemozilla::gfx::GradientCacheData91   nsExpirationState* GetExpirationState() { return &mExpirationState; }
92 
93   nsExpirationState mExpirationState;
94   const RefPtr<GradientStops> mStops;
95   GradientCacheKey mKey;
96 };
97 
98 /**
99  * This class implements a cache, that retains the GradientStops used to draw
100  * the gradients.
101  *
102  * An entry stays in the cache as long as it is used often and we don't exceed
103  * the maximum, in which case the most recently used will be kept.
104  */
105 class GradientCache;
106 using GradientCacheMutex = StaticDataMutex<UniquePtr<GradientCache>>;
107 class MOZ_RAII LockedInstance {
108  public:
LockedInstance(GradientCacheMutex & aDataMutex)109   explicit LockedInstance(GradientCacheMutex& aDataMutex)
110       : mAutoLock(aDataMutex.Lock()) {}
operator ->() const111   UniquePtr<GradientCache>& operator->() const& { return mAutoLock.ref(); }
112   UniquePtr<GradientCache>& operator->() const&& = delete;
operator *() const113   UniquePtr<GradientCache>& operator*() const& { return mAutoLock.ref(); }
114   UniquePtr<GradientCache>& operator*() const&& = delete;
operator bool() const115   explicit operator bool() const { return !!mAutoLock.ref(); }
116 
117  private:
118   GradientCacheMutex::AutoLock mAutoLock;
119 };
120 
121 class GradientCache final
122     : public ExpirationTrackerImpl<GradientCacheData, 4, GradientCacheMutex,
123                                    LockedInstance> {
124  public:
GradientCache()125   GradientCache()
126       : ExpirationTrackerImpl<GradientCacheData, 4, GradientCacheMutex,
127                               LockedInstance>(MAX_GENERATION_MS,
128                                               "GradientCache") {}
EnsureInstance()129   static bool EnsureInstance() {
130     LockedInstance lockedInstance(sInstanceMutex);
131     return EnsureInstanceLocked(lockedInstance);
132   }
133 
DestroyInstance()134   static void DestroyInstance() {
135     LockedInstance lockedInstance(sInstanceMutex);
136     if (lockedInstance) {
137       *lockedInstance = nullptr;
138     }
139   }
140 
AgeAllGenerations()141   static void AgeAllGenerations() {
142     LockedInstance lockedInstance(sInstanceMutex);
143     if (!lockedInstance) {
144       return;
145     }
146     lockedInstance->AgeAllGenerationsLocked(lockedInstance);
147     lockedInstance->NotifyHandlerEndLocked(lockedInstance);
148   }
149 
Lookup(const nsTArray<GradientStop> & aStops,ExtendMode aExtend,BackendType aBackendType)150   static GradientCacheData* Lookup(const nsTArray<GradientStop>& aStops,
151                                    ExtendMode aExtend,
152                                    BackendType aBackendType) {
153     LockedInstance lockedInstance(sInstanceMutex);
154     if (!EnsureInstanceLocked(lockedInstance)) {
155       return nullptr;
156     }
157 
158     GradientCacheData* gradientData = lockedInstance->mHashEntries.Get(
159         GradientCacheKey(aStops, aExtend, aBackendType));
160     if (!gradientData) {
161       return nullptr;
162     }
163 
164     if (!gradientData->mStops || !gradientData->mStops->IsValid()) {
165       lockedInstance->NotifyExpiredLocked(gradientData, lockedInstance);
166       return nullptr;
167     }
168 
169     lockedInstance->MarkUsedLocked(gradientData, lockedInstance);
170     return gradientData;
171   }
172 
RegisterEntry(UniquePtr<GradientCacheData> aValue)173   static void RegisterEntry(UniquePtr<GradientCacheData> aValue) {
174     uint32_t numberOfEntries;
175     {
176       LockedInstance lockedInstance(sInstanceMutex);
177       if (!EnsureInstanceLocked(lockedInstance)) {
178         return;
179       }
180 
181       nsresult rv =
182           lockedInstance->AddObjectLocked(aValue.get(), lockedInstance);
183       if (NS_FAILED(rv)) {
184         // We are OOM, and we cannot track this object. We don't want to store
185         // entries in the hash table (since the expiration tracker is
186         // responsible for removing the cache entries), so we avoid putting that
187         // entry in the table, which is a good thing considering we are short on
188         // memory anyway, we probably don't want to retain things.
189         return;
190       }
191       lockedInstance->mHashEntries.InsertOrUpdate(aValue->mKey,
192                                                   std::move(aValue));
193       numberOfEntries = lockedInstance->mHashEntries.Count();
194     }
195     if (numberOfEntries > MAX_ENTRIES) {
196       // We have too many entries force the cache to age a generation.
197       NS_DispatchToMainThread(
198           NS_NewRunnableFunction("GradientCache::OnMaxEntriesBreached", [] {
199             LockedInstance lockedInstance(sInstanceMutex);
200             if (!lockedInstance) {
201               return;
202             }
203             lockedInstance->AgeOneGenerationLocked(lockedInstance);
204             lockedInstance->NotifyHandlerEndLocked(lockedInstance);
205           }));
206     }
207   }
208 
GetMutex()209   GradientCacheMutex& GetMutex() final { return sInstanceMutex; }
210 
NotifyExpiredLocked(GradientCacheData * aObject,const LockedInstance & aLockedInstance)211   void NotifyExpiredLocked(GradientCacheData* aObject,
212                            const LockedInstance& aLockedInstance) final {
213     // Remove the gradient from the tracker.
214     RemoveObjectLocked(aObject, aLockedInstance);
215 
216     // If entry exists move the data to mRemovedGradientData because we want to
217     // drop it outside of the lock.
218     Maybe<UniquePtr<GradientCacheData>> gradientData =
219         mHashEntries.Extract(aObject->mKey);
220     if (gradientData.isSome()) {
221       mRemovedGradientData.AppendElement(std::move(*gradientData));
222     }
223   }
224 
NotifyHandlerEndLocked(const LockedInstance &)225   void NotifyHandlerEndLocked(const LockedInstance&) final {
226     NS_DispatchToCurrentThread(
227         NS_NewRunnableFunction("GradientCache::DestroyRemovedGradientStops",
228                                [stops = std::move(mRemovedGradientData)] {}));
229   }
230 
231  private:
232   static const uint32_t MAX_GENERATION_MS = 10000;
233 
234   // On Windows some of the Direct2D objects associated with the gradient stops
235   // can be quite large, so we limit the number of cache entries.
236   static const uint32_t MAX_ENTRIES = 4000;
237   static GradientCacheMutex sInstanceMutex;
238 
EnsureInstanceLocked(LockedInstance & aLockedInstance)239   [[nodiscard]] static bool EnsureInstanceLocked(
240       LockedInstance& aLockedInstance) {
241     if (!aLockedInstance) {
242       // GradientCache must be created on the main thread.
243       if (!NS_IsMainThread()) {
244         // This should only happen at shutdown, we fall back to not caching.
245         return false;
246       }
247       *aLockedInstance = MakeUnique<GradientCache>();
248     }
249     return true;
250   }
251 
252   /**
253    * FIXME use nsTHashtable to avoid duplicating the GradientCacheKey.
254    * https://bugzilla.mozilla.org/show_bug.cgi?id=761393#c47
255    */
256   nsClassHashtable<GradientCacheKey, GradientCacheData> mHashEntries;
257   nsTArray<UniquePtr<GradientCacheData>> mRemovedGradientData;
258 };
259 
260 GradientCacheMutex GradientCache::sInstanceMutex("GradientCache");
261 
Init()262 void gfxGradientCache::Init() {
263   MOZ_RELEASE_ASSERT(GradientCache::EnsureInstance(),
264                      "First call must be on main thread.");
265 }
266 
GetOrCreateGradientStops(const DrawTarget * aDT,nsTArray<GradientStop> & aStops,ExtendMode aExtend)267 already_AddRefed<GradientStops> gfxGradientCache::GetOrCreateGradientStops(
268     const DrawTarget* aDT, nsTArray<GradientStop>& aStops, ExtendMode aExtend) {
269   if (aDT->IsRecording()) {
270     return aDT->CreateGradientStops(aStops.Elements(), aStops.Length(),
271                                     aExtend);
272   }
273 
274   GradientCacheData* cached =
275       GradientCache::Lookup(aStops, aExtend, aDT->GetBackendType());
276   if (cached) {
277     return do_AddRef(cached->mStops);
278   }
279 
280   RefPtr<GradientStops> gs =
281       aDT->CreateGradientStops(aStops.Elements(), aStops.Length(), aExtend);
282   if (!gs) {
283     return nullptr;
284   }
285   GradientCache::RegisterEntry(MakeUnique<GradientCacheData>(
286       gs, GradientCacheKey(aStops, aExtend, aDT->GetBackendType())));
287   return gs.forget();
288 }
289 
PurgeAllCaches()290 void gfxGradientCache::PurgeAllCaches() { GradientCache::AgeAllGenerations(); }
291 
Shutdown()292 void gfxGradientCache::Shutdown() { GradientCache::DestroyInstance(); }
293 
294 }  // namespace gfx
295 }  // namespace mozilla
296