From 0f147ac2ae575bbad3515a526f13700bc5c8e9d7 Mon Sep 17 00:00:00 2001 From: bsalomon Date: Mon, 3 Oct 2016 14:07:01 -0700 Subject: [PATCH] Make GrResourceCache dynamically change between LRU and random replacement strategies. Random performs significantly better when each frame exceeds the budget by a small margin whereas LRU has worst case behavior. The decision of which to use is made based on the history from a few frames of the ratio of total unique key cache misses to unique key cache misses of resources purged in the last 2 frames. GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2321563006 Review-Url: https://codereview.chromium.org/2321563006 --- include/gpu/GrGpuResource.h | 2 + src/gpu/GrDrawingManager.cpp | 14 +++- src/gpu/GrDrawingManager.h | 2 + src/gpu/GrResourceCache.cpp | 122 +++++++++++++++++++++++++++++------ src/gpu/GrResourceCache.h | 70 +++++++++++++++----- 5 files changed, 173 insertions(+), 37 deletions(-) diff --git a/include/gpu/GrGpuResource.h b/include/gpu/GrGpuResource.h index 364a886408..6f8280acb2 100644 --- a/include/gpu/GrGpuResource.h +++ b/include/gpu/GrGpuResource.h @@ -58,6 +58,7 @@ public: this->validate(); if (!(--fRefCnt)) { + SkASSERT(fRefCnt >= 0); if (!static_cast(this)->notifyRefCountIsZero()) { return; } @@ -117,6 +118,7 @@ private: private: void didRemoveRefOrPendingIO(CntType cntTypeRemoved) const { + this->validate(); if (0 == fPendingReads && 0 == fPendingWrites && 0 == fRefCnt) { static_cast(this)->notifyAllCntsAreZero(cntTypeRemoved); } diff --git a/src/gpu/GrDrawingManager.cpp b/src/gpu/GrDrawingManager.cpp index 5e9b68d88f..75195670b2 100644 --- a/src/gpu/GrDrawingManager.cpp +++ b/src/gpu/GrDrawingManager.cpp @@ -129,10 +129,17 @@ void GrDrawingManager::internalFlush(GrResourceCache::FlushType type) { #endif fFlushState.reset(); - // We always have to notify the cache when it requested a flush so it can reset its state. - if (flushed || type == GrResourceCache::FlushType::kCacheRequested) { + // Avoid notifying the cache about successive client flushes where no rendering occurred between + // them. + bool skipNotify = false; + if (!flushed) { + skipNotify = GrResourceCache::kExternal == fLastFlushType && + GrResourceCache::kExternal == type; + } + if (!skipNotify) { fContext->getResourceCache()->notifyFlushOccurred(type); } + fLastFlushType = type; fFlushing = false; } @@ -145,6 +152,9 @@ void GrDrawingManager::prepareSurfaceForExternalIO(GrSurface* surface) { if (surface->surfacePriv().hasPendingIO()) { this->flush(); + } else if (GrResourceCache::kExternal != fLastFlushType) { + fContext->getResourceCache()->notifyFlushOccurred(GrResourceCache::kExternal); + fLastFlushType = GrResourceCache::kExternal; } GrRenderTarget* rt = surface->asRenderTarget(); diff --git a/src/gpu/GrDrawingManager.h b/src/gpu/GrDrawingManager.h index 9fced38163..3e90d3bd17 100644 --- a/src/gpu/GrDrawingManager.h +++ b/src/gpu/GrDrawingManager.h @@ -76,6 +76,7 @@ private: , fSoftwarePathRenderer(nullptr) , fFlushState(context->getGpu(), context->resourceProvider()) , fFlushing(false) + , fLastFlushType(GrResourceCache::FlushType::kExternal) , fIsImmediateMode(isImmediateMode) { } @@ -107,6 +108,7 @@ private: GrBatchFlushState fFlushState; bool fFlushing; + GrResourceCache::FlushType fLastFlushType; bool fIsImmediateMode; }; diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp index 9462a7384d..e6afa81b79 100644 --- a/src/gpu/GrResourceCache.cpp +++ b/src/gpu/GrResourceCache.cpp @@ -57,13 +57,22 @@ private: }; ////////////////////////////////////////////////////////////////////////////// - +constexpr int GrResourceCache::kStrategyScoreMin; +constexpr int GrResourceCache::kStrategyScoreMax; +constexpr int GrResourceCache::kInitialStrategyScore; GrResourceCache::GrResourceCache(const GrCaps* caps) : fTimestamp(0) , fMaxCount(kDefaultMaxCount) , fMaxBytes(kDefaultMaxSize) , fMaxUnusedFlushes(kDefaultMaxUnusedFlushes) + , fStrategy(ReplacementStrategy::kLRU) + , fStrategyScore(kInitialStrategyScore) + , fTotalMissesThisFlush(0) + , fMissesThisFlushPurgedRecently(0) + , fUniqueKeysPurgedThisFlushStorage {new SkChunkAlloc(8*sizeof(GrUniqueKey)), + new SkChunkAlloc(8*sizeof(GrUniqueKey))} + , fFlushParity(0) #if GR_CACHE_STATS , fHighWaterCount(0) , fHighWaterBytes(0) @@ -73,8 +82,8 @@ GrResourceCache::GrResourceCache(const GrCaps* caps) , fBytes(0) , fBudgetedCount(0) , fBudgetedBytes(0) - , fRequestFlush(false) , fExternalFlushCnt(0) + , fIsPurging(false) , fPreferVRAMUseOverFlushes(caps->preferVRAMUseOverFlushes()) { SkDEBUGCODE(fCount = 0;) SkDEBUGCODE(fNewlyPurgeableResourceForValidation = nullptr;) @@ -82,6 +91,8 @@ GrResourceCache::GrResourceCache(const GrCaps* caps) GrResourceCache::~GrResourceCache() { this->releaseAll(); + delete fUniqueKeysPurgedThisFlushStorage[0]; + delete fUniqueKeysPurgedThisFlushStorage[1]; } void GrResourceCache::setLimits(int count, size_t bytes, int maxUnusedFlushes) { @@ -227,8 +238,10 @@ private: GrGpuResource* GrResourceCache::findAndRefScratchResource(const GrScratchKey& scratchKey, size_t resourceSize, uint32_t flags) { + // We don't currently track misses for scratch resources for selecting the replacement policy. + // The reason is that it is common to look for a scratch resource before creating a texture + // that will immediately become uniquely keyed. SkASSERT(scratchKey.isValid()); - GrGpuResource* resource; if (flags & (kPreferNoPendingIO_ScratchFlag | kRequireNoPendingIO_ScratchFlag)) { resource = fScratchMap.find(scratchKey, AvailableForScratchUse(true)); @@ -256,6 +269,25 @@ GrGpuResource* GrResourceCache::findAndRefScratchResource(const GrScratchKey& sc return resource; } +GrGpuResource* GrResourceCache::findAndRefUniqueResource(const GrUniqueKey& key) { + GrGpuResource* resource = fUniqueHash.find(key); + if (resource) { + this->refAndMakeResourceMRU(resource); + } else { + this->recordKeyMiss(key); + } + return resource; +} + +void GrResourceCache::recordKeyMiss(const GrUniqueKey& key) { + // If a resource with this key was purged either this flush or the previous flush, consider it + // a recent purge. + if (fUniqueKeysPurgedThisFlush[0].find(key) || fUniqueKeysPurgedThisFlush[1].find(key)) { + ++fMissesThisFlushPurgedRecently; + } + ++fTotalMissesThisFlush; +} + void GrResourceCache::willRemoveScratchKey(const GrGpuResource* resource) { SkASSERT(resource->resourcePriv().getScratchKey().isValid()); if (!resource->getUniqueKey().isValid()) { @@ -380,9 +412,12 @@ void GrResourceCache::notifyCntReachedZero(GrGpuResource* resource, uint32_t fla } else { // Purge the resource immediately if we're over budget // Also purge if the resource has neither a valid scratch key nor a unique key. - bool noKey = !resource->resourcePriv().getScratchKey().isValid() && - !resource->getUniqueKey().isValid(); - if (!this->overBudget() && !noKey) { + bool hasKey = resource->resourcePriv().getScratchKey().isValid() || + resource->getUniqueKey().isValid(); + if (hasKey) { + if (this->overBudget()) { + this->purgeAsNeeded(); + } return; } } @@ -442,7 +477,36 @@ void GrResourceCache::didChangeBudgetStatus(GrGpuResource* resource) { this->validate(); } -void GrResourceCache::purgeAsNeeded() { +void GrResourceCache::recordPurgedKey(GrGpuResource* resource) { + // This maximum exists to avoid allocating too much space for key tracking. + static constexpr int kMaxTrackedKeys = 256; + if (fUniqueKeysPurgedThisFlush[fFlushParity].count() >= kMaxTrackedKeys) { + return; + } + if (resource->getUniqueKey().isValid() && + !fUniqueKeysPurgedThisFlush[fFlushParity].find(resource->getUniqueKey())) { + void* p = fUniqueKeysPurgedThisFlushStorage[fFlushParity]->allocThrow(sizeof(GrUniqueKey)); + GrUniqueKey* copy = new (p) GrUniqueKey; + *copy = resource->getUniqueKey(); + fUniqueKeysPurgedThisFlush[fFlushParity].add(copy); + } +} + +GrGpuResource* GrResourceCache::selectResourceUsingStrategy() { + switch (fStrategy) { + case ReplacementStrategy::kLRU: + return fPurgeableQueue.peek(); + case ReplacementStrategy::kRandom: + return fPurgeableQueue.at(fRandom.nextULessThan(fPurgeableQueue.count())); + } + return nullptr; +} + +void GrResourceCache::internalPurgeAsNeeded(bool fromFlushNotification) { + if (fIsPurging) { + return; + } + fIsPurging = true; SkTArray invalidKeyMsgs; fInvalidUniqueKeyInbox.poll(&invalidKeyMsgs); if (invalidKeyMsgs.count()) { @@ -470,26 +534,31 @@ void GrResourceCache::purgeAsNeeded() { } GrGpuResource* resource = fPurgeableQueue.peek(); SkASSERT(resource->isPurgeable()); + this->recordPurgedKey(resource); resource->cacheAccess().release(); } } } + if (ReplacementStrategy::kRandom == fStrategy && !fromFlushNotification) { + // Wait until after the requested flush when all the pending IO resources will be eligible + // for the draft. + SkASSERT(!this->overBudget() || this->requestsFlush()); + fIsPurging = false; + return; + } + bool stillOverbudget = this->overBudget(); while (stillOverbudget && fPurgeableQueue.count()) { - GrGpuResource* resource = fPurgeableQueue.peek(); + GrGpuResource* resource = this->selectResourceUsingStrategy(); SkASSERT(resource->isPurgeable()); + this->recordPurgedKey(resource); resource->cacheAccess().release(); stillOverbudget = this->overBudget(); } this->validate(); - - if (stillOverbudget) { - // Set this so that GrDrawingManager will issue a flush to free up resources with pending - // IO that we were unable to purge in this pass. - fRequestFlush = true; - } + fIsPurging = false; } void GrResourceCache::purgeAllUnlocked() { @@ -549,7 +618,6 @@ uint32_t GrResourceCache::getNextTimestamp() { *sortedPurgeableResources.append() = fPurgeableQueue.peek(); fPurgeableQueue.pop(); } - SkTQSort(fNonpurgeableResources.begin(), fNonpurgeableResources.end() - 1, CompareTimestamp); @@ -600,10 +668,25 @@ void GrResourceCache::notifyFlushOccurred(FlushType type) { case FlushType::kImmediateMode: break; case FlushType::kCacheRequested: - SkASSERT(fRequestFlush); - fRequestFlush = false; break; - case FlushType::kExternal: + case FlushType::kExternal: { + int scoreDelta = 1; + if (fMissesThisFlushPurgedRecently) { + // If > 60% of our cache misses were things we purged in the last two flushes + // then we move closer towards selecting random replacement. + if ((float)fMissesThisFlushPurgedRecently / fTotalMissesThisFlush > 0.6f) { + scoreDelta = -1; + } + } + fStrategyScore = SkTPin(fStrategyScore + scoreDelta, kStrategyScoreMin, + kStrategyScoreMax); + fStrategy = fStrategyScore < 0 ? ReplacementStrategy::kRandom + : ReplacementStrategy::kLRU; + fMissesThisFlushPurgedRecently = 0; + fTotalMissesThisFlush = 0; + fFlushParity = -(fFlushParity - 1); + fUniqueKeysPurgedThisFlush[fFlushParity].reset(); + fUniqueKeysPurgedThisFlushStorage[fFlushParity]->rewind(); ++fExternalFlushCnt; if (0 == fExternalFlushCnt) { // When this wraps just reset all the purgeable resources' last used flush state. @@ -612,8 +695,9 @@ void GrResourceCache::notifyFlushOccurred(FlushType type) { } } break; + } } - this->purgeAsNeeded(); + this->internalPurgeAsNeeded(true); } void GrResourceCache::dumpMemoryStatistics(SkTraceMemoryDump* traceMemoryDump) const { diff --git a/src/gpu/GrResourceCache.h b/src/gpu/GrResourceCache.h index ae9a4e7ee7..d6cdf81612 100644 --- a/src/gpu/GrResourceCache.h +++ b/src/gpu/GrResourceCache.h @@ -13,7 +13,9 @@ #include "GrGpuResourcePriv.h" #include "GrResourceCache.h" #include "GrResourceKey.h" +#include "SkChunkAlloc.h" #include "SkMessageBus.h" +#include "SkRandom.h" #include "SkRefCnt.h" #include "SkTArray.h" #include "SkTDPQueue.h" @@ -137,13 +139,7 @@ public: /** * Find a resource that matches a unique key. */ - GrGpuResource* findAndRefUniqueResource(const GrUniqueKey& key) { - GrGpuResource* resource = fUniqueHash.find(key); - if (resource) { - this->refAndMakeResourceMRU(resource); - } - return resource; - } + GrGpuResource* findAndRefUniqueResource(const GrUniqueKey& key); /** * Query whether a unique key exists in the cache. @@ -154,14 +150,19 @@ public: /** Purges resources to become under budget and processes resources with invalidated unique keys. */ - void purgeAsNeeded(); + void purgeAsNeeded() { this->internalPurgeAsNeeded(false); } /** Purges all resources that don't have external owners. */ void purgeAllUnlocked(); /** Returns true if the cache would like a flush to occur in order to make more resources purgeable. */ - bool requestsFlush() const { return fRequestFlush; } + bool requestsFlush() const { + // When in random replacement mode we request a flush in order to make as many resources + // as possible subject to replacement. + return this->overBudget() && (ReplacementStrategy::kRandom == fStrategy || + 0 == fPurgeableQueue.count()); + } enum FlushType { kExternal, @@ -232,10 +233,14 @@ private: void refAndMakeResourceMRU(GrGpuResource*); /// @} + void internalPurgeAsNeeded(bool fromFlushNotification); void processInvalidUniqueKeys(const SkTArray&); void addToNonpurgeableArray(GrGpuResource*); void removeFromNonpurgeableArray(GrGpuResource*); bool overBudget() const { return fBudgetedBytes > fMaxBytes || fBudgetedCount > fMaxCount; } + GrGpuResource* selectResourceUsingStrategy(); + void recordPurgedKey(GrGpuResource*); + void recordKeyMiss(const GrUniqueKey&); bool wouldFit(size_t bytes) { return fBudgetedBytes+bytes <= fMaxBytes && fBudgetedCount+1 <= fMaxCount; @@ -254,22 +259,27 @@ private: class AvailableForScratchUse; - struct ScratchMapTraits { + struct HashTraitsBase { + static uint32_t Hash(const GrResourceKey& key) { return key.hash(); } + }; + + struct ScratchMapTraits : public HashTraitsBase { static const GrScratchKey& GetKey(const GrGpuResource& r) { return r.resourcePriv().getScratchKey(); } - - static uint32_t Hash(const GrScratchKey& key) { return key.hash(); } }; typedef SkTMultiMap ScratchMap; - struct UniqueHashTraits { + struct UniqueHashTraits : public HashTraitsBase { static const GrUniqueKey& GetKey(const GrGpuResource& r) { return r.getUniqueKey(); } - - static uint32_t Hash(const GrUniqueKey& key) { return key.hash(); } }; typedef SkTDynamicHash UniqueHash; + struct UniqueSetTraits : public HashTraitsBase { + static const GrUniqueKey& GetKey(const GrUniqueKey& key) { return key; } + }; + typedef SkTDynamicHash UniqueKeySet; + static bool CompareTimestamp(GrGpuResource* const& a, GrGpuResource* const& b) { return a->cacheAccess().timestamp() < b->cacheAccess().timestamp(); } @@ -278,6 +288,22 @@ private: return res->cacheAccess().accessCacheIndex(); } + /** + * The resource cache chooses one of these replacement strategies based on a "strategy score" + * updated after each external flush based on unique key cache misses. + */ + enum class ReplacementStrategy { + kLRU, + kRandom + }; + /** + * When the current strategy score is >=0 LRU is chosen, when it is < 0 random is chosen. The + * absolute value of the score moves by 1 each flush. + */ + static constexpr int kStrategyScoreMin = -5; + static constexpr int kStrategyScoreMax = 4; + static constexpr int kInitialStrategyScore = 2; + typedef SkMessageBus::Inbox InvalidUniqueKeyInbox; typedef SkTDPQueue PurgeableQueue; typedef SkTDArray ResourceArray; @@ -299,6 +325,17 @@ private: size_t fMaxBytes; int fMaxUnusedFlushes; + // Data related to replacement strategy. + SkRandom fRandom; + ReplacementStrategy fStrategy; + int fStrategyScore; + int fTotalMissesThisFlush; + int fMissesThisFlushPurgedRecently; + UniqueKeySet fUniqueKeysPurgedThisFlush[2]; + // These are pointers to SkChunckAlloc because of gcc bug 63707 + SkChunkAlloc* fUniqueKeysPurgedThisFlushStorage[2]; + int fFlushParity; + #if GR_CACHE_STATS int fHighWaterCount; size_t fHighWaterBytes; @@ -314,9 +351,10 @@ private: int fBudgetedCount; size_t fBudgetedBytes; - bool fRequestFlush; uint32_t fExternalFlushCnt; + bool fIsPurging; + InvalidUniqueKeyInbox fInvalidUniqueKeyInbox; // This resource is allowed to be in the nonpurgeable array for the sake of validate() because