Bug 1697344 p1: Make GradientCache thread safe. r=jrmuizel

This is so that we can use it in the canvas worker threads.
It also sets a maximum number of entries because on Windows the associated
Direct2D objects can be fairly big.

Differential Revision: https://phabricator.services.mozilla.com/D109790
This commit is contained in:
Bob Owen 2021-03-29 12:12:20 +00:00
parent 695a760d5c
commit 3155e74038
3 changed files with 166 additions and 78 deletions

View File

@ -3,12 +3,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gfxGradientCache.h"
#include "MainThreadUtils.h"
#include "mozilla/gfx/2D.h" #include "mozilla/gfx/2D.h"
#include "mozilla/DataMutex.h"
#include "nsTArray.h" #include "nsTArray.h"
#include "PLDHashTable.h" #include "PLDHashTable.h"
#include "nsExpirationTracker.h" #include "nsExpirationTracker.h"
#include "nsClassHashtable.h" #include "nsClassHashtable.h"
#include "gfxGradientCache.h"
#include <time.h> #include <time.h>
namespace mozilla { namespace mozilla {
@ -93,85 +96,172 @@ struct GradientCacheData {
}; };
/** /**
* This class implements a cache with no maximum size, that retains the * This class implements a cache, that retains the GradientStops used to draw
* gfxPatterns used to draw the gradients. * the gradients.
* *
* The key is the nsStyleGradient that defines the gradient, and the size of the * An entry stays in the cache as long as it is used often and we don't exceed
* gradient. * the maximum, in which case the most recently used will be kept.
*
* The value is the gfxPattern, and whether or not we perform an optimization
* based on the actual gradient property.
*
* An entry stays in the cache as long as it is used often. As long as a cache
* entry is in the cache, all the references it has are guaranteed to be valid:
* the nsStyleRect for the key, the gfxPattern for the value.
*/ */
class GradientCache final : public nsExpirationTracker<GradientCacheData, 4> { class GradientCache;
using GradientCacheMutex = StaticDataMutex<UniquePtr<GradientCache>>;
class MOZ_RAII LockedInstance {
public:
explicit LockedInstance(GradientCacheMutex& aDataMutex)
: mAutoLock(aDataMutex.Lock()) {}
UniquePtr<GradientCache>& operator->() const& { return mAutoLock.ref(); }
UniquePtr<GradientCache>& operator->() const&& = delete;
UniquePtr<GradientCache>& operator*() const& { return mAutoLock.ref(); }
UniquePtr<GradientCache>& operator*() const&& = delete;
explicit operator bool() const { return !!mAutoLock.ref(); }
private:
GradientCacheMutex::AutoLock mAutoLock;
};
class GradientCache final
: public ExpirationTrackerImpl<GradientCacheData, 4, GradientCacheMutex,
LockedInstance> {
public: public:
GradientCache() GradientCache()
: nsExpirationTracker<GradientCacheData, 4>(MAX_GENERATION_MS, : ExpirationTrackerImpl<GradientCacheData, 4, GradientCacheMutex,
"GradientCache") { LockedInstance>(MAX_GENERATION_MS,
srand(time(nullptr)); "GradientCache") {}
static bool EnsureInstance() {
LockedInstance lockedInstance(sInstanceMutex);
return EnsureInstanceLocked(lockedInstance);
} }
virtual void NotifyExpired(GradientCacheData* aObject) override { static void DestroyInstance() {
// This will free the gfxPattern. LockedInstance lockedInstance(sInstanceMutex);
RemoveObject(aObject); if (lockedInstance) {
mHashEntries.Remove(aObject->mKey); *lockedInstance = nullptr;
}
GradientCacheData* Lookup(const nsTArray<GradientStop>& aStops,
ExtendMode aExtend, BackendType aBackendType) {
GradientCacheData* gradient =
mHashEntries.Get(GradientCacheKey(aStops, aExtend, aBackendType));
if (gradient) {
MarkUsed(gradient);
} }
return gradient;
} }
void RegisterEntry(UniquePtr<GradientCacheData> aValue) { static void AgeAllGenerations() {
nsresult rv = AddObject(aValue.get()); LockedInstance lockedInstance(sInstanceMutex);
if (NS_FAILED(rv)) { if (!lockedInstance) {
// We are OOM, and we cannot track this object. We don't want stall
// entries in the hash table (since the expiration tracker is responsible
// for removing the cache entries), so we avoid putting that entry in the
// table, which is a good thing considering we are short on memory
// anyway, we probably don't want to retain things.
return; return;
} }
mHashEntries.InsertOrUpdate(aValue->mKey, std::move(aValue)); lockedInstance->AgeAllGenerationsLocked(lockedInstance);
lockedInstance->NotifyHandlerEndLocked(lockedInstance);
} }
protected: static GradientCacheData* Lookup(const nsTArray<GradientStop>& aStops,
ExtendMode aExtend,
BackendType aBackendType) {
LockedInstance lockedInstance(sInstanceMutex);
if (!EnsureInstanceLocked(lockedInstance)) {
return nullptr;
}
GradientCacheData* gradientData = lockedInstance->mHashEntries.Get(
GradientCacheKey(aStops, aExtend, aBackendType));
if (!gradientData) {
return nullptr;
}
if (!gradientData->mStops || !gradientData->mStops->IsValid()) {
lockedInstance->NotifyExpiredLocked(gradientData, lockedInstance);
return nullptr;
}
lockedInstance->MarkUsedLocked(gradientData, lockedInstance);
return gradientData;
}
static void RegisterEntry(UniquePtr<GradientCacheData> aValue) {
uint32_t numberOfEntries;
{
LockedInstance lockedInstance(sInstanceMutex);
if (!EnsureInstanceLocked(lockedInstance)) {
return;
}
nsresult rv =
lockedInstance->AddObjectLocked(aValue.get(), lockedInstance);
if (NS_FAILED(rv)) {
// We are OOM, and we cannot track this object. We don't want to store
// entries in the hash table (since the expiration tracker is
// responsible for removing the cache entries), so we avoid putting that
// entry in the table, which is a good thing considering we are short on
// memory anyway, we probably don't want to retain things.
return;
}
lockedInstance->mHashEntries.InsertOrUpdate(aValue->mKey,
std::move(aValue));
numberOfEntries = lockedInstance->mHashEntries.Count();
}
if (numberOfEntries > MAX_ENTRIES) {
// We have too many entries force the cache to age a generation.
NS_DispatchToMainThread(
NS_NewRunnableFunction("GradientCache::OnMaxEntriesBreached", [] {
LockedInstance lockedInstance(sInstanceMutex);
if (!lockedInstance) {
return;
}
lockedInstance->AgeOneGenerationLocked(lockedInstance);
lockedInstance->NotifyHandlerEndLocked(lockedInstance);
}));
}
}
GradientCacheMutex& GetMutex() final { return sInstanceMutex; }
void NotifyExpiredLocked(GradientCacheData* aObject,
const LockedInstance& aLockedInstance) final {
// Remove the gradient from the tracker.
RemoveObjectLocked(aObject, aLockedInstance);
// If entry exists move the data to mRemovedGradientData because we want to
// drop it outside of the lock.
Maybe<UniquePtr<GradientCacheData>> gradientData =
mHashEntries.Extract(aObject->mKey);
if (gradientData.isSome()) {
mRemovedGradientData.AppendElement(std::move(*gradientData));
}
}
void NotifyHandlerEndLocked(const LockedInstance&) final {
NS_DispatchToCurrentThread(
NS_NewRunnableFunction("GradientCache::DestroyRemovedGradientStops",
[stops = std::move(mRemovedGradientData)] {}));
}
private:
static const uint32_t MAX_GENERATION_MS = 10000; static const uint32_t MAX_GENERATION_MS = 10000;
// On Windows some of the Direct2D objects associated with the gradient stops
// can be quite large, so we limit the number of cache entries.
static const uint32_t MAX_ENTRIES = 4000;
static GradientCacheMutex sInstanceMutex;
[[nodiscard]] static bool EnsureInstanceLocked(
LockedInstance& aLockedInstance) {
if (!aLockedInstance) {
// GradientCache must be created on the main thread.
if (!NS_IsMainThread()) {
// This should only happen at shutdown, we fall back to not caching.
return false;
}
*aLockedInstance = MakeUnique<GradientCache>();
}
return true;
}
/** /**
* FIXME use nsTHashtable to avoid duplicating the GradientCacheKey. * FIXME use nsTHashtable to avoid duplicating the GradientCacheKey.
* https://bugzilla.mozilla.org/show_bug.cgi?id=761393#c47 * https://bugzilla.mozilla.org/show_bug.cgi?id=761393#c47
*/ */
nsClassHashtable<GradientCacheKey, GradientCacheData> mHashEntries; nsClassHashtable<GradientCacheKey, GradientCacheData> mHashEntries;
nsTArray<UniquePtr<GradientCacheData>> mRemovedGradientData;
}; };
static GradientCache* gGradientCache = nullptr; GradientCacheMutex GradientCache::sInstanceMutex("GradientCache");
GradientStops* gfxGradientCache::GetGradientStops( void gfxGradientCache::Init() {
const DrawTarget* aDT, nsTArray<GradientStop>& aStops, ExtendMode aExtend) { MOZ_RELEASE_ASSERT(GradientCache::EnsureInstance(),
if (!gGradientCache) { "First call must be on main thread.");
gGradientCache = new GradientCache();
}
GradientCacheData* cached =
gGradientCache->Lookup(aStops, aExtend, aDT->GetBackendType());
if (cached && cached->mStops) {
if (!cached->mStops->IsValid()) {
gGradientCache->NotifyExpired(cached);
} else {
return cached->mStops;
}
}
return nullptr;
} }
already_AddRefed<GradientStops> gfxGradientCache::GetOrCreateGradientStops( already_AddRefed<GradientStops> gfxGradientCache::GetOrCreateGradientStops(
@ -181,28 +271,25 @@ already_AddRefed<GradientStops> gfxGradientCache::GetOrCreateGradientStops(
aExtend); aExtend);
} }
RefPtr<GradientStops> gs = GetGradientStops(aDT, aStops, aExtend); GradientCacheData* cached =
if (!gs) { GradientCache::Lookup(aStops, aExtend, aDT->GetBackendType());
gs = aDT->CreateGradientStops(aStops.Elements(), aStops.Length(), aExtend); if (cached) {
if (!gs) { return do_AddRef(cached->mStops);
return nullptr;
}
gGradientCache->RegisterEntry(MakeUnique<GradientCacheData>(
gs, GradientCacheKey(aStops, aExtend, aDT->GetBackendType())));
} }
RefPtr<GradientStops> gs =
aDT->CreateGradientStops(aStops.Elements(), aStops.Length(), aExtend);
if (!gs) {
return nullptr;
}
GradientCache::RegisterEntry(MakeUnique<GradientCacheData>(
gs, GradientCacheKey(aStops, aExtend, aDT->GetBackendType())));
return gs.forget(); return gs.forget();
} }
void gfxGradientCache::PurgeAllCaches() { void gfxGradientCache::PurgeAllCaches() { GradientCache::AgeAllGenerations(); }
if (gGradientCache) {
gGradientCache->AgeAllGenerations();
}
}
void gfxGradientCache::Shutdown() { void gfxGradientCache::Shutdown() { GradientCache::DestroyInstance(); }
delete gGradientCache;
gGradientCache = nullptr;
}
} // namespace gfx } // namespace gfx
} // namespace mozilla } // namespace mozilla

View File

@ -16,9 +16,7 @@ namespace gfx {
class gfxGradientCache { class gfxGradientCache {
public: public:
static gfx::GradientStops* GetGradientStops( static void Init();
const gfx::DrawTarget* aDT, nsTArray<gfx::GradientStop>& aStops,
gfx::ExtendMode aExtend);
static already_AddRefed<gfx::GradientStops> GetOrCreateGradientStops( static already_AddRefed<gfx::GradientStops> GetOrCreateGradientStops(
const gfx::DrawTarget* aDT, nsTArray<gfx::GradientStop>& aStops, const gfx::DrawTarget* aDT, nsTArray<gfx::GradientStop>& aStops,

View File

@ -94,6 +94,9 @@ struct nsExpirationState {
* For creating a thread-safe tracker, you can define a subclass inheriting this * For creating a thread-safe tracker, you can define a subclass inheriting this
* base class and specialize the Mutex and AutoLock to be used. * base class and specialize the Mutex and AutoLock to be used.
* *
* For an example of using ExpirationTrackerImpl with a DataMutex
* @see mozilla::gfx::GradientCache.
*
*/ */
template <typename T, uint32_t K, typename Mutex, typename AutoLock> template <typename T, uint32_t K, typename Mutex, typename AutoLock>
class ExpirationTrackerImpl { class ExpirationTrackerImpl {