Add mechanism to proactively purge old resources in GrResourceCache.
authorbsalomon <bsalomon@google.com>
Wed, 8 Apr 2015 18:01:54 +0000 (11:01 -0700)
committerCommit bot <commit-bot@chromium.org>
Wed, 8 Apr 2015 18:01:55 +0000 (11:01 -0700)
This change leaves the feature turned off by default.

Review URL: https://codereview.chromium.org/1032873002

include/gpu/GrGpuResource.h
src/gpu/GrContext.cpp
src/gpu/GrGpuResource.cpp
src/gpu/GrResourceCache.cpp
src/gpu/GrResourceCache.h
tests/ResourceCacheTest.cpp

index e2f23e0..5a35ab7 100644 (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
index 89429f1..48e50b7 100755 (executable)
@@ -1477,6 +1477,7 @@ void GrContext::flush(int flagsBitfield) {
     } else {
         fDrawBuffer->flush();
     }
+    fResourceCache->notifyFlushOccurred();
     fFlushToReduceCacheSize = false;
 }
 
index a2fc7b3..52fe1e5 100644 (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) {
index f8b1a12..f1a51b0 100644 (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);
index 8331bf5..5483e19 100644 (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.
+     */
+    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 notifyPurgeable(GrGpuResource* resource) { fCache->notifyPurgeable(resource); }
+    void notifyCntReachedZero(GrGpuResource* resource, uint32_t flags) {
+        fCache->notifyCntReachedZero(resource, flags);
+    }
 
     /**
      * Called by GrGpuResources when their sizes change.
index 737d743..f92ba87 100644 (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);
 }