Add a new GrResourceCache purging mechanism for purging unused resources.

The client may call GrContext::purgeResourceNotUsedSince() with a stead_clock::time_point and all resources that have been purgeable since before that time point are purged.

This is intended to replace the "max unused flushes" purging mechanism once Chrome adopts it.

Change-Id: I28881dd2959cc01c0acca81b2d6001ee5626439d
Reviewed-on: https://skia-review.googlesource.com/8920
Commit-Queue: Brian Salomon <bsalomon@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Reviewed-by: Eric Karl <ericrk@google.com>
diff --git a/include/gpu/GrContext.h b/include/gpu/GrContext.h
index 57500cd..ca378d7 100644
--- a/include/gpu/GrContext.h
+++ b/include/gpu/GrContext.h
@@ -160,6 +160,15 @@
      */
     void purgeAllUnlockedResources();
 
+    /**
+     * Purge GPU resources that haven't been used since the passed in time, regardless of whether
+     * the context is currently under budget.
+     */
+    void purgeResourcesNotUsedSince(std::chrono::steady_clock::time_point);
+    void purgeResourcesNotUsedInMs(std::chrono::milliseconds ms) {
+        this->purgeResourcesNotUsedSince(std::chrono::steady_clock::now() - ms);
+    }
+
     /** Access the context capabilities */
     const GrCaps* caps() const { return fCaps; }
 
diff --git a/include/gpu/GrGpuResource.h b/include/gpu/GrGpuResource.h
index e0a7903..1c4cbfa 100644
--- a/include/gpu/GrGpuResource.h
+++ b/include/gpu/GrGpuResource.h
@@ -8,6 +8,7 @@
 #ifndef GrGpuResource_DEFINED
 #define GrGpuResource_DEFINED
 
+#include <chrono>
 #include "GrResourceKey.h"
 #include "GrTypesPriv.h"
 
@@ -315,28 +316,30 @@
     void makeUnbudgeted();
 
 #ifdef SK_DEBUG
-    friend class GrGpu; // for assert in GrGpu to access getGpu
+    friend class GrGpu;  // for assert in GrGpu to access getGpu
 #endif
+
     // An index into a heap when this resource is purgeable or an array when not. This is maintained
     // by the cache.
-    int                         fCacheArrayIndex;
+    int fCacheArrayIndex;
     // This value reflects how recently this resource was accessed in the cache. This is maintained
     // by the cache.
-    uint32_t                    fTimestamp;
-    uint32_t                    fExternalFlushCntWhenBecamePurgeable;
+    uint32_t fTimestamp;
+    uint32_t fExternalFlushCntWhenBecamePurgeable;
+    std::chrono::steady_clock::time_point fTimeWhenBecamePurgeable;
 
     static const size_t kInvalidGpuMemorySize = ~static_cast<size_t>(0);
-    GrScratchKey                fScratchKey;
-    GrUniqueKey                 fUniqueKey;
+    GrScratchKey fScratchKey;
+    GrUniqueKey fUniqueKey;
 
     // This is not ref'ed but abandon() or release() will be called before the GrGpu object
     // is destroyed. Those calls set will this to NULL.
-    GrGpu*                      fGpu;
-    mutable size_t              fGpuMemorySize;
+    GrGpu* fGpu;
+    mutable size_t fGpuMemorySize;
 
-    SkBudgeted                  fBudgeted;
-    bool                        fRefsWrappedObjects;
-    const UniqueID              fUniqueID;
+    SkBudgeted fBudgeted;
+    bool fRefsWrappedObjects;
+    const UniqueID fUniqueID;
 
     typedef GrIORef<GrGpuResource> INHERITED;
     friend class GrIORef<GrGpuResource>; // to access notifyAllCntsAreZero and notifyRefCntIsZero.
diff --git a/src/gpu/GrContext.cpp b/src/gpu/GrContext.cpp
index bcb93b3..efce0b2 100644
--- a/src/gpu/GrContext.cpp
+++ b/src/gpu/GrContext.cpp
@@ -193,6 +193,11 @@
     fResourceCache->purgeAllUnlocked();
 }
 
+void GrContext::purgeResourcesNotUsedSince(std::chrono::steady_clock::time_point purgeTime) {
+    ASSERT_SINGLE_OWNER
+    fResourceCache->purgeResourcesNotUsedSince(purgeTime);
+}
+
 void GrContext::getResourceCacheUsage(int* resourceCount, size_t* resourceBytes) const {
     ASSERT_SINGLE_OWNER
 
diff --git a/src/gpu/GrGpuResourceCacheAccess.h b/src/gpu/GrGpuResourceCacheAccess.h
index e91f899..e679aff 100644
--- a/src/gpu/GrGpuResourceCacheAccess.h
+++ b/src/gpu/GrGpuResourceCacheAccess.h
@@ -63,6 +63,10 @@
         SkASSERT(fResource->isPurgeable());
         fResource->fExternalFlushCntWhenBecamePurgeable = cnt;
     }
+    void setTimeWhenResourceBecomePurgeable() {
+        SkASSERT(fResource->isPurgeable());
+        fResource->fTimeWhenBecamePurgeable = std::chrono::steady_clock::now();
+    }
     /**
      * Called by the cache to determine whether this resource has been puregable for more than
      * a threshold number of external flushes.
@@ -71,6 +75,14 @@
         SkASSERT(fResource->isPurgeable());
         return fResource->fExternalFlushCntWhenBecamePurgeable;
     }
+    /**
+     * Called by the cache to determine whether this resource should be purged based on the length
+     * of time it has been available for purging.
+     */
+    std::chrono::steady_clock::time_point timeWhenResourceBecamePurgeable() {
+        SkASSERT(fResource->isPurgeable());
+        return fResource->fTimeWhenBecamePurgeable;
+    }
 
     int* accessCacheIndex() const { return &fResource->fCacheArrayIndex; }
 
diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp
index 9462a73..97d3b96 100644
--- a/src/gpu/GrResourceCache.cpp
+++ b/src/gpu/GrResourceCache.cpp
@@ -365,6 +365,7 @@
     this->removeFromNonpurgeableArray(resource);
     fPurgeableQueue.insert(resource);
     resource->cacheAccess().setFlushCntWhenResourceBecamePurgeable(fExternalFlushCnt);
+    resource->cacheAccess().setTimeWhenResourceBecomePurgeable();
 
     if (SkBudgeted::kNo == resource->resourcePriv().isBudgeted()) {
         // Check whether this resource could still be used as a scratch resource.
@@ -504,6 +505,24 @@
     this->validate();
 }
 
+void GrResourceCache::purgeResourcesNotUsedSince(std::chrono::steady_clock::time_point purgeTime) {
+    while (fPurgeableQueue.count()) {
+        const std::chrono::steady_clock::time_point resourceTime =
+                fPurgeableQueue.peek()->cacheAccess().timeWhenResourceBecamePurgeable();
+        if (resourceTime >= purgeTime) {
+            // Resources were given both LRU timestamps and tagged with a frame number when
+            // they first became purgeable. The LRU timestamp won't change again until the
+            // resource is made non-purgeable again. So, at this point all the remaining
+            // resources in the timestamp-sorted queue will have a frame number >= to this
+            // one.
+            break;
+        }
+        GrGpuResource* resource = fPurgeableQueue.peek();
+        SkASSERT(resource->isPurgeable());
+        resource->cacheAccess().release();
+    }
+}
+
 void GrResourceCache::processInvalidUniqueKeys(
     const SkTArray<GrUniqueKeyInvalidatedMessage>& msgs) {
     for (int i = 0; i < msgs.count(); ++i) {
diff --git a/src/gpu/GrResourceCache.h b/src/gpu/GrResourceCache.h
index 5f08a51..aef9a5c 100644
--- a/src/gpu/GrResourceCache.h
+++ b/src/gpu/GrResourceCache.h
@@ -50,8 +50,9 @@
     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 external flushes a budgeted resources can go unused in the cache before it 
-    // is purged. Using a value <= 0 disables this feature.
+    // Default number of external flushes a budgeted resources can go unused in the cache before it
+    // is purged. Using a value <= 0 disables this feature. This will be removed once Chrome
+    // starts using time-based purging.
     static const int    kDefaultMaxUnusedFlushes =
             1  * /* flushes per frame */
             60 * /* fps */
@@ -159,6 +160,9 @@
     /** Purges all resources that don't have external owners. */
     void purgeAllUnlocked();
 
+    /** Purge all resources not used since the passed in time. */
+    void purgeResourcesNotUsedSince(std::chrono::steady_clock::time_point);
+
     /** Returns true if the cache would like a flush to occur in order to make more resources
         purgeable. */
     bool requestsFlush() const { return fRequestFlush; }
diff --git a/tests/ResourceCacheTest.cpp b/tests/ResourceCacheTest.cpp
index 8051191..7766a6a 100644
--- a/tests/ResourceCacheTest.cpp
+++ b/tests/ResourceCacheTest.cpp
@@ -1241,6 +1241,98 @@
     REPORTER_ASSERT(reporter, 10 == cache->getResourceCount());
 }
 
+static void test_time_purge(skiatest::Reporter* reporter) {
+    Mock mock(1000000, 1000000);
+    GrContext* context = mock.context();
+    GrResourceCache* cache = mock.cache();
+
+    static constexpr int kCnts[] = {1, 10, 1024};
+    for (int cnt : kCnts) {
+        std::unique_ptr<std::chrono::steady_clock::time_point[]> timeStamps(
+                new std::chrono::steady_clock::time_point[cnt]);
+        {
+            // Insert resources and get time points between each addition.
+            for (int i = 0; i < cnt; ++i) {
+                TestResource* r = new TestResource(context->getGpu());
+                GrUniqueKey k;
+                make_unique_key<1>(&k, i);
+                r->resourcePriv().setUniqueKey(k);
+                r->unref();
+                timeStamps.get()[i] = std::chrono::steady_clock::now();
+            }
+
+            // Purge based on the time points between resource additions. Each purge should remove
+            // the oldest resource.
+            for (int i = 0; i < cnt; ++i) {
+                cache->purgeResourcesNotUsedSince(timeStamps[i]);
+                REPORTER_ASSERT(reporter, cnt - 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);
+                }
+            }
+
+            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.
+        {
+            std::unique_ptr<GrGpuResource* []> refedResources(new GrGpuResource*[cnt / 2]);
+            for (int i = 0; i < cnt; ++i) {
+                TestResource* r = new 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.get()[i / 2] = r;
+                } else {
+                    r->unref();
+                }
+                timeStamps.get()[i] = std::chrono::steady_clock::now();
+            }
+
+            for (int i = 0; i < cnt; ++i) {
+                // Should get a resource purged every other frame.
+                cache->purgeResourcesNotUsedSince(timeStamps[i]);
+                REPORTER_ASSERT(reporter, cnt - i / 2 - 1 == cache->getResourceCount());
+            }
+
+            // Unref all the resources that we kept refs on in the first loop.
+            for (int i = 0; i < (cnt / 2); ++i) {
+                refedResources.get()[i]->unref();
+                context->purgeResourcesNotUsedSince(std::chrono::steady_clock::now());
+                REPORTER_ASSERT(reporter, cnt / 2 - i - 1 == cache->getResourceCount());
+            }
+
+            cache->purgeAllUnlocked();
+        }
+
+        REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
+
+        // Verify that calling flush() on a GrContext with nothing to do will not trigger resource
+        // eviction
+        context->flush();
+        for (int i = 0; i < 10; ++i) {
+            TestResource* r = new TestResource(context->getGpu());
+            GrUniqueKey k;
+            make_unique_key<1>(&k, i);
+            r->resourcePriv().setUniqueKey(k);
+            r->unref();
+        }
+        REPORTER_ASSERT(reporter, 10 == cache->getResourceCount());
+        context->flush();
+        REPORTER_ASSERT(reporter, 10 == cache->getResourceCount());
+        context->purgeResourcesNotUsedSince(std::chrono::steady_clock::now());
+        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
@@ -1393,6 +1485,7 @@
     test_resource_size_changed(reporter);
     test_timestamp_wrap(reporter);
     test_flush(reporter);
+    test_time_purge(reporter);
     test_large_resource_count(reporter);
     test_custom_data(reporter);
     test_abandoned(reporter);