Add mechanism to proactively purge old resources in GrResourceCache.

This change leaves the feature turned off by default.

Review URL: https://codereview.chromium.org/1032873002
This commit is contained in:
bsalomon 2015-04-08 11:01:54 -07:00 committed by Commit bot
parent b7133bed55
commit 3f324321cd
6 changed files with 307 additions and 51 deletions

View File

@ -33,8 +33,15 @@ class GrResourceCache;
*
* The latter two ref types are private and intended only for Gr core code.
*
* When an item is purgeable DERIVED:notifyIsPurgeable() will be called (static poly morphism using
* CRTP). GrIORef and GrGpuResource are separate classes for organizational reasons and to be
* When all the ref/io counts reach zero DERIVED::notifyAllCntsAreZero() will be called (static poly
* morphism using CRTP). Similarly when the ref (but not necessarily pending read/write) count
* reaches 0 DERIVED::notifyRefCountIsZero() will be called. In the case when an unref() causes both
* the ref cnt to reach zero and the other counts are zero, notifyRefCountIsZero() will be called
* before notifyIsPurgeable(). Moreover, if notifyRefCountIsZero() returns false then
* notifyAllRefCntsAreZero() won't be called at all. notifyRefCountIsZero() must return false if the
* object may be deleted after notifyRefCntIsZero() returns.
*
* GrIORef and GrGpuResource are separate classes for organizational reasons and to be
* able to give access via friendship to only the functions related to pending IO operations.
*/
template <typename DERIVED> class GrIORef : public SkNoncopyable {
@ -52,8 +59,14 @@ public:
void unref() const {
this->validate();
--fRefCnt;
this->didUnref();
if (!(--fRefCnt)) {
if (!static_cast<const DERIVED*>(this)->notifyRefCountIsZero()) {
return;
}
}
this->didRemoveRefOrPendingIO(kRef_CntType);
}
void validate() const {
@ -68,6 +81,12 @@ public:
protected:
GrIORef() : fRefCnt(1), fPendingReads(0), fPendingWrites(0) { }
enum CntType {
kRef_CntType,
kPendingRead_CntType,
kPendingWrite_CntType,
};
bool isPurgeable() const { return !this->internalHasRef() && !this->internalHasPendingIO(); }
bool internalHasPendingRead() const { return SkToBool(fPendingReads); }
@ -85,7 +104,7 @@ private:
void completedRead() const {
this->validate();
--fPendingReads;
this->didUnref();
this->didRemoveRefOrPendingIO(kPendingRead_CntType);
}
void addPendingWrite() const {
@ -96,13 +115,13 @@ private:
void completedWrite() const {
this->validate();
--fPendingWrites;
this->didUnref();
this->didRemoveRefOrPendingIO(kPendingWrite_CntType);
}
private:
void didUnref() const {
void didRemoveRefOrPendingIO(CntType cntTypeRemoved) const {
if (0 == fPendingReads && 0 == fPendingWrites && 0 == fRefCnt) {
static_cast<const DERIVED*>(this)->notifyIsPurgeable();
static_cast<const DERIVED*>(this)->notifyAllCntsAreZero(cntTypeRemoved);
}
}
@ -271,7 +290,8 @@ private:
// See comments in CacheAccess and ResourcePriv.
void setUniqueKey(const GrUniqueKey&);
void removeUniqueKey();
void notifyIsPurgeable() const;
void notifyAllCntsAreZero(CntType) const;
bool notifyRefCountIsZero() const;
void removeScratchKey();
void makeBudgeted();
void makeUnbudgeted();
@ -304,7 +324,7 @@ private:
SkAutoTUnref<const SkData> fData;
typedef GrIORef<GrGpuResource> INHERITED;
friend class GrIORef<GrGpuResource>; // to access notifyIsPurgeable.
friend class GrIORef<GrGpuResource>; // to access notifyAllCntsAreZero and notifyRefCntIsZero.
};
#endif

View File

@ -1477,6 +1477,7 @@ void GrContext::flush(int flagsBitfield) {
} else {
fDrawBuffer->flush();
}
fResourceCache->notifyFlushOccurred();
fFlushToReduceCacheSize = false;
}

View File

@ -105,14 +105,39 @@ void GrGpuResource::setUniqueKey(const GrUniqueKey& key) {
get_resource_cache(fGpu)->resourceAccess().changeUniqueKey(this, key);
}
void GrGpuResource::notifyIsPurgeable() const {
void GrGpuResource::notifyAllCntsAreZero(CntType lastCntTypeToReachZero) const {
if (this->wasDestroyed()) {
// We've already been removed from the cache. Goodbye cruel world!
SkDELETE(this);
} else {
GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
get_resource_cache(fGpu)->resourceAccess().notifyPurgeable(mutableThis);
return;
}
// We should have already handled this fully in notifyRefCntIsZero().
SkASSERT(kRef_CntType != lastCntTypeToReachZero);
GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
static const uint32_t kFlag =
GrResourceCache::ResourceAccess::kAllCntsReachedZero_RefNotificationFlag;
get_resource_cache(fGpu)->resourceAccess().notifyCntReachedZero(mutableThis, kFlag);
}
bool GrGpuResource::notifyRefCountIsZero() const {
if (this->wasDestroyed()) {
// handle this in notifyAllCntsAreZero().
return true;
}
GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
uint32_t flags =
GrResourceCache::ResourceAccess::kRefCntReachedZero_RefNotificationFlag;
if (!this->internalHasPendingIO()) {
flags |= GrResourceCache::ResourceAccess::kAllCntsReachedZero_RefNotificationFlag;
}
get_resource_cache(fGpu)->resourceAccess().notifyCntReachedZero(mutableThis, flags);
// There is no need to call our notifyAllCntsAreZero function at this point since we already
// told the cache about the state of cnts.
return false;
}
void GrGpuResource::setScratchKey(const GrScratchKey& scratchKey) {

View File

@ -40,6 +40,7 @@ GrUniqueKey::Domain GrUniqueKey::GenerateDomain() {
return static_cast<Domain>(domain);
}
uint32_t GrResourceKeyHash(const uint32_t* data, size_t size) {
return SkChecksum::Compute(data, size);
}
@ -56,13 +57,12 @@ private:
//////////////////////////////////////////////////////////////////////////////
static const int kDefaultMaxCount = 2 * (1 << 12);
static const size_t kDefaultMaxSize = 96 * (1 << 20);
GrResourceCache::GrResourceCache()
: fTimestamp(0)
, fMaxCount(kDefaultMaxCount)
, fMaxBytes(kDefaultMaxSize)
, fMaxUnusedFlushes(kDefaultMaxUnusedFlushes)
#if GR_CACHE_STATS
, fHighWaterCount(0)
, fHighWaterBytes(0)
@ -73,20 +73,49 @@ GrResourceCache::GrResourceCache()
, fBudgetedCount(0)
, fBudgetedBytes(0)
, fOverBudgetCB(NULL)
, fOverBudgetData(NULL) {
, fOverBudgetData(NULL)
, fFlushTimestamps(NULL)
, fLastFlushTimestampIndex(0){
SkDEBUGCODE(fCount = 0;)
SkDEBUGCODE(fNewlyPurgeableResourceForValidation = NULL;)
this->resetFlushTimestamps();
}
GrResourceCache::~GrResourceCache() {
this->releaseAll();
SkDELETE(fFlushTimestamps);
}
void GrResourceCache::setLimits(int count, size_t bytes) {
void GrResourceCache::setLimits(int count, size_t bytes, int maxUnusedFlushes) {
fMaxCount = count;
fMaxBytes = bytes;
fMaxUnusedFlushes = maxUnusedFlushes;
this->resetFlushTimestamps();
this->purgeAsNeeded();
}
void GrResourceCache::resetFlushTimestamps() {
SkDELETE(fFlushTimestamps);
// We assume this number is a power of two when wrapping indices into the timestamp array.
fMaxUnusedFlushes = SkNextPow2(fMaxUnusedFlushes);
// Since our implementation is to store the timestamps of the last fMaxUnusedFlushes flush calls
// we just turn the feature off if that array would be large.
static const int kMaxSupportedTimestampHistory = 128;
if (fMaxUnusedFlushes > kMaxSupportedTimestampHistory) {
fFlushTimestamps = NULL;
return;
}
fFlushTimestamps = SkNEW_ARRAY(uint32_t, fMaxUnusedFlushes);
fLastFlushTimestampIndex = 0;
// Set all the historical flush timestamps to initially be at the beginning of time (timestamp
// 0).
sk_bzero(fFlushTimestamps, fMaxUnusedFlushes * sizeof(uint32_t));
}
void GrResourceCache::insertResource(GrGpuResource* resource) {
SkASSERT(resource);
SkASSERT(!this->isInCache(resource));
@ -247,8 +276,8 @@ void GrResourceCache::willRemoveScratchKey(const GrGpuResource* resource) {
}
void GrResourceCache::removeUniqueKey(GrGpuResource* resource) {
// Someone has a ref to this resource in order to invalidate it. When the ref count reaches
// zero we will get a notifyPurgable() and figure out what to do with it.
// Someone has a ref to this resource in order to have removed the key. When the ref count
// reaches zero we will get a ref cnt notification and figure out what to do with it.
if (resource->getUniqueKey().isValid()) {
SkASSERT(resource == fUniqueHash.find(resource->getUniqueKey()));
fUniqueHash.remove(resource->getUniqueKey());
@ -307,11 +336,34 @@ void GrResourceCache::refAndMakeResourceMRU(GrGpuResource* resource) {
this->validate();
}
void GrResourceCache::notifyPurgeable(GrGpuResource* resource) {
void GrResourceCache::notifyCntReachedZero(GrGpuResource* resource, uint32_t flags) {
SkASSERT(resource);
SkASSERT(!resource->wasDestroyed());
SkASSERT(flags);
SkASSERT(this->isInCache(resource));
SkASSERT(resource->isPurgeable());
// This resource should always be in the nonpurgeable array when this function is called. It
// will be moved to the queue if it is newly purgeable.
SkASSERT(fNonpurgeableResources[*resource->cacheAccess().accessCacheIndex()] == resource);
if (SkToBool(ResourceAccess::kRefCntReachedZero_RefNotificationFlag & flags)) {
#ifdef SK_DEBUG
// When the timestamp overflows validate() is called. validate() checks that resources in
// the nonpurgeable array are indeed not purgeable. However, the movement from the array to
// the purgeable queue happens just below in this function. So we mark it as an exception.
if (resource->isPurgeable()) {
fNewlyPurgeableResourceForValidation = resource;
}
#endif
resource->cacheAccess().setTimestamp(this->getNextTimestamp());
SkDEBUGCODE(fNewlyPurgeableResourceForValidation = NULL);
}
if (!SkToBool(ResourceAccess::kAllCntsReachedZero_RefNotificationFlag & flags)) {
SkASSERT(!resource->isPurgeable());
return;
}
SkASSERT(resource->isPurgeable());
this->removeFromNonpurgeableArray(resource);
fPurgeableQueue.insert(resource);
@ -391,25 +443,43 @@ void GrResourceCache::didChangeBudgetStatus(GrGpuResource* resource) {
this->validate();
}
void GrResourceCache::internalPurgeAsNeeded() {
SkASSERT(this->overBudget());
void GrResourceCache::purgeAsNeeded() {
SkTArray<GrUniqueKeyInvalidatedMessage> invalidKeyMsgs;
fInvalidUniqueKeyInbox.poll(&invalidKeyMsgs);
if (invalidKeyMsgs.count()) {
this->processInvalidUniqueKeys(invalidKeyMsgs);
}
bool stillOverbudget = true;
while (fPurgeableQueue.count()) {
if (fFlushTimestamps) {
// Assuming kNumFlushesToDeleteUnusedResource is a power of 2.
SkASSERT(SkIsPow2(fMaxUnusedFlushes));
int oldestFlushIndex = (fLastFlushTimestampIndex + 1) & (fMaxUnusedFlushes - 1);
uint32_t oldestAllowedTimestamp = fFlushTimestamps[oldestFlushIndex];
while (fPurgeableQueue.count()) {
uint32_t oldestResourceTimestamp = fPurgeableQueue.peek()->cacheAccess().timestamp();
if (oldestAllowedTimestamp < oldestResourceTimestamp) {
break;
}
GrGpuResource* resource = fPurgeableQueue.peek();
SkASSERT(resource->isPurgeable());
resource->cacheAccess().release();
}
}
bool stillOverbudget = this->overBudget();
while (stillOverbudget && fPurgeableQueue.count()) {
GrGpuResource* resource = fPurgeableQueue.peek();
SkASSERT(resource->isPurgeable());
resource->cacheAccess().release();
if (!this->overBudget()) {
stillOverbudget = false;
break;
}
stillOverbudget = this->overBudget();
}
this->validate();
if (stillOverbudget) {
// Despite the purge we're still over budget. Call our over budget callback. If this frees
// any resources then we'll get notifyPurgeable() calls and take appropriate action.
// any resources then we'll get notified and take appropriate action.
(*fOverBudgetCB)(fOverBudgetData);
this->validate();
}
@ -433,7 +503,7 @@ void GrResourceCache::processInvalidUniqueKeys(
GrGpuResource* resource = this->findAndRefUniqueResource(msgs[i].key());
if (resource) {
resource->resourcePriv().removeUniqueKey();
resource->unref(); // will call notifyPurgeable, if it is indeed now purgeable.
resource->unref(); // If this resource is now purgeable, the cache will be notified.
}
}
}
@ -518,11 +588,26 @@ uint32_t GrResourceCache::getNextTimestamp() {
// count should be the next timestamp we return.
SkASSERT(fTimestamp == SkToU32(count));
// The historical timestamps of flushes are now invalid.
this->resetFlushTimestamps();
}
}
return fTimestamp++;
}
void GrResourceCache::notifyFlushOccurred() {
if (fFlushTimestamps) {
SkASSERT(SkIsPow2(fMaxUnusedFlushes));
fLastFlushTimestampIndex = (fLastFlushTimestampIndex + 1) & (fMaxUnusedFlushes - 1);
// get the timestamp before accessing fFlushTimestamps because getNextTimestamp will
// reallocate fFlushTimestamps on timestamp overflow.
uint32_t timestamp = this->getNextTimestamp();
fFlushTimestamps[fLastFlushTimestampIndex] = timestamp;
this->purgeAsNeeded();
}
}
#ifdef SK_DEBUG
void GrResourceCache::validate() const {
// Reduce the frequency of validations for large resource counts.
@ -586,7 +671,8 @@ void GrResourceCache::validate() const {
Stats stats(this);
for (int i = 0; i < fNonpurgeableResources.count(); ++i) {
SkASSERT(!fNonpurgeableResources[i]->isPurgeable());
SkASSERT(!fNonpurgeableResources[i]->isPurgeable() ||
fNewlyPurgeableResourceForValidation == fNonpurgeableResources[i]);
SkASSERT(*fNonpurgeableResources[i]->cacheAccess().accessCacheIndex() == i);
SkASSERT(!fNonpurgeableResources[i]->wasDestroyed());
stats.update(fNonpurgeableResources[i]);
@ -615,7 +701,7 @@ void GrResourceCache::validate() const {
SkASSERT(stats.fContent == fUniqueHash.count());
SkASSERT(stats.fScratch + stats.fCouldBeScratch == fScratchMap.count());
// This assertion is not currently valid because we can be in recursive notifyIsPurgeable()
// This assertion is not currently valid because we can be in recursive notifyCntReachedZero()
// calls. This will be fixed when subresource registration is explicit.
// bool overBudget = budgetedBytes > fMaxBytes || budgetedCount > fMaxCount;
// SkASSERT(!overBudget || locked == count || fPurging);

View File

@ -38,20 +38,39 @@ class SkString;
* A unique key always takes precedence over a scratch key when a resource has both types of keys.
* If a resource has neither key type then it will be deleted as soon as the last reference to it
* is dropped.
*
* When proactive purging is enabled, on every flush, the timestamp of that flush is stored in a
* n-sized ring buffer. When purging occurs each purgeable resource's timestamp is compared to the
* timestamp of the n-th prior flush. If the resource's last use timestamp is older than the old
* flush then the resource is proactively purged even when the cache is under budget. By default
* this feature is disabled, though it can be enabled by calling GrResourceCache::setLimits.
*/
class GrResourceCache {
public:
GrResourceCache();
~GrResourceCache();
// Default maximum number of budgeted resources in the cache.
static const int kDefaultMaxCount = 2 * (1 << 12);
// Default maximum number of bytes of gpu memory of budgeted resources in the cache.
static const size_t kDefaultMaxSize = 96 * (1 << 20);
// Default number of flushes a budgeted resources can go unused in the cache before it is
// purged. Large values disable the feature (as the ring buffer of flush timestamps would be
// large). This is currently the default until we decide to enable this feature
// of the cache by default.
static const int kDefaultMaxUnusedFlushes = 1024;
/** Used to access functionality needed by GrGpuResource for lifetime management. */
class ResourceAccess;
ResourceAccess resourceAccess();
/**
* Sets the cache limits in terms of number of resources and max gpu memory byte size.
* Sets the cache limits in terms of number of resources, max gpu memory byte size, and number
* of GrContext flushes that a resource can be unused before it is evicted. The latter value is
* a suggestion and there is no promise that a resource will be purged immediately after it
* hasn't been used in maxUnusedFlushes flushes.
*/
void setLimits(int count, size_t bytes);
void setLimits(int count, size_t bytes, int maxUnusedFlushes = kDefaultMaxUnusedFlushes);
/**
* Returns the number of resources.
@ -136,17 +155,7 @@ public:
/** Purges resources to become under budget and processes resources with invalidated unique
keys. */
void purgeAsNeeded() {
SkTArray<GrUniqueKeyInvalidatedMessage> invalidKeyMsgs;
fInvalidUniqueKeyInbox.poll(&invalidKeyMsgs);
if (invalidKeyMsgs.count()) {
this->processInvalidUniqueKeys(invalidKeyMsgs);
}
if (fBudgetedCount <= fMaxCount && fBudgetedBytes <= fMaxBytes) {
return;
}
this->internalPurgeAsNeeded();
}
void purgeAsNeeded();
/** Purges all resources that don't have external owners. */
void purgeAllUnlocked();
@ -166,6 +175,8 @@ public:
fOverBudgetCB = overBudgetCB;
fOverBudgetData = data;
}
void notifyFlushOccurred();
#if GR_GPU_STATS
void dumpStats(SkString*) const;
@ -180,7 +191,7 @@ private:
////
void insertResource(GrGpuResource*);
void removeResource(GrGpuResource*);
void notifyPurgeable(GrGpuResource*);
void notifyCntReachedZero(GrGpuResource*, uint32_t flags);
void didChangeGpuMemorySize(const GrGpuResource*, size_t oldSize);
void changeUniqueKey(GrGpuResource*, const GrUniqueKey&);
void removeUniqueKey(GrGpuResource*);
@ -189,7 +200,7 @@ private:
void refAndMakeResourceMRU(GrGpuResource*);
/// @}
void internalPurgeAsNeeded();
void resetFlushTimestamps();
void processInvalidUniqueKeys(const SkTArray<GrUniqueKeyInvalidatedMessage>&);
void addToNonpurgeableArray(GrGpuResource*);
void removeFromNonpurgeableArray(GrGpuResource*);
@ -251,6 +262,7 @@ private:
// our budget, used in purgeAsNeeded()
int fMaxCount;
size_t fMaxBytes;
int fMaxUnusedFlushes;
#if GR_CACHE_STATS
int fHighWaterCount;
@ -270,7 +282,16 @@ private:
PFOverBudgetCB fOverBudgetCB;
void* fOverBudgetData;
// We keep track of the "timestamps" of the last n flushes. If a resource hasn't been used in
// that time then we well preemptively purge it to reduce memory usage.
uint32_t* fFlushTimestamps;
int fLastFlushTimestampIndex;
InvalidUniqueKeyInbox fInvalidUniqueKeyInbox;
// This resource is allowed to be in the nonpurgeable array for the sake of validate() because
// we're in the midst of converting it to purgeable status.
SkDEBUGCODE(GrGpuResource* fNewlyPurgeableResourceForValidation;)
};
class GrResourceCache::ResourceAccess {
@ -290,9 +311,26 @@ private:
void removeResource(GrGpuResource* resource) { fCache->removeResource(resource); }
/**
* Called by GrGpuResources when they detects that they are newly purgeable.
* Notifications that should be sent to the cache when the ref/io cnt status of resources
* changes.
*/
void notifyPurgeable(GrGpuResource* resource) { fCache->notifyPurgeable(resource); }
enum RefNotificationFlags {
/** All types of refs on the resource have reached zero. */
kAllCntsReachedZero_RefNotificationFlag = 0x1,
/** The normal (not pending IO type) ref cnt has reached zero. */
kRefCntReachedZero_RefNotificationFlag = 0x2,
};
/**
* Called by GrGpuResources when they detect that their ref/io cnts have reached zero. When the
* normal ref cnt reaches zero the flags that are set should be:
* a) kRefCntReachedZero if a pending IO cnt is still non-zero.
* b) (kRefCntReachedZero | kAllCntsReachedZero) when all pending IO cnts are also zero.
* kAllCntsReachedZero is set by itself if a pending IO cnt is decremented to zero and all the
* the other cnts are already zero.
*/
void notifyCntReachedZero(GrGpuResource* resource, uint32_t flags) {
fCache->notifyCntReachedZero(resource, flags);
}
/**
* Called by GrGpuResources when their sizes change.

View File

@ -5,6 +5,9 @@
* found in the LICENSE file.
*/
// Include here to ensure SK_SUPPORT_GPU is set correctly before it is examined.
#include "SkTypes.h"
#if SK_SUPPORT_GPU
#include "GrContext.h"
@ -1023,6 +1026,88 @@ static void test_timestamp_wrap(skiatest::Reporter* reporter) {
}
}
static void test_flush(skiatest::Reporter* reporter) {
Mock mock(1000000, 1000000);
GrContext* context = mock.context();
GrResourceCache* cache = mock.cache();
// The current cache impl will round the max flush count to the next power of 2. So we choose a
// power of two here to keep things simpler.
static const int kFlushCount = 16;
cache->setLimits(1000000, 1000000, kFlushCount);
{
// Insert a resource and send a flush notification kFlushCount times.
for (int i = 0; i < kFlushCount; ++i) {
TestResource* r = SkNEW_ARGS(TestResource, (context->getGpu()));
GrUniqueKey k;
make_unique_key<1>(&k, i);
r->resourcePriv().setUniqueKey(k);
r->unref();
cache->notifyFlushOccurred();
}
// Send flush notifications to the cache. Each flush should purge the oldest resource.
for (int i = 0; i < kFlushCount - 1; ++i) {
// The first resource was purged after the last flush in the initial loop, hence the -1.
REPORTER_ASSERT(reporter, kFlushCount - i - 1 == cache->getResourceCount());
for (int j = 0; j < i; ++j) {
GrUniqueKey k;
make_unique_key<1>(&k, j);
GrGpuResource* r = cache->findAndRefUniqueResource(k);
REPORTER_ASSERT(reporter, !SkToBool(r));
SkSafeUnref(r);
}
cache->notifyFlushOccurred();
}
REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
cache->purgeAllUnlocked();
}
// Do a similar test but where we leave refs on some resources to prevent them from being
// purged.
{
GrGpuResource* refedResources[kFlushCount >> 1];
for (int i = 0; i < kFlushCount; ++i) {
TestResource* r = SkNEW_ARGS(TestResource, (context->getGpu()));
GrUniqueKey k;
make_unique_key<1>(&k, i);
r->resourcePriv().setUniqueKey(k);
// Leave a ref on every other resource, beginning with the first.
if (SkToBool(i & 0x1)) {
refedResources[i/2] = r;
} else {
r->unref();
}
cache->notifyFlushOccurred();
}
for (int i = 0; i < kFlushCount; ++i) {
// Should get a resource purged every other flush.
REPORTER_ASSERT(reporter, kFlushCount - i/2 - 1 == cache->getResourceCount());
cache->notifyFlushOccurred();
}
// Unref all the resources that we kept refs on in the first loop.
for (int i = 0; i < kFlushCount >> 1; ++i) {
refedResources[i]->unref();
}
// When we unref'ed them their timestamps got updated. So nothing should be purged until we
// get kFlushCount additional flushes. Then everything should be purged.
for (int i = 0; i < kFlushCount; ++i) {
REPORTER_ASSERT(reporter, kFlushCount >> 1 == cache->getResourceCount());
cache->notifyFlushOccurred();
}
REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
cache->purgeAllUnlocked();
}
REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
}
static void test_large_resource_count(skiatest::Reporter* reporter) {
// Set the cache size to double the resource count because we're going to create 2x that number
// resources, using two different key domains. Add a little slop to the bytes because we resize
@ -1118,6 +1203,7 @@ DEF_GPUTEST(ResourceCache, reporter, factory) {
test_cache_chained_purge(reporter);
test_resource_size_changed(reporter);
test_timestamp_wrap(reporter);
test_flush(reporter);
test_large_resource_count(reporter);
}