Add method to GrContext to purge unlocked resources.

Beyond setting the total cache limits this method enables clients to
request to purge a specific number of bytes, as well as specify their
preference to purge scratch resources over resources of other types.

Change-Id: I9259d5544d34251575d77eebe599388f213ff3ce
Reviewed-on: https://skia-review.googlesource.com/17987
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Derek Sollenberger <djsollen@google.com>
diff --git a/include/gpu/GrContext.h b/include/gpu/GrContext.h
index 5bd9574..0f3d925 100644
--- a/include/gpu/GrContext.h
+++ b/include/gpu/GrContext.h
@@ -172,6 +172,18 @@
      */
     void purgeResourcesNotUsedInMs(std::chrono::milliseconds ms);
 
+    /**
+     * Purge unlocked resources from the cache until the the provided byte count has been reached
+     * or we have purged all unlocked resources. The default policy is to purge in LRU order, but
+     * can be overridden to prefer purging scratch resources (in LRU order) prior to purging other
+     * resource types.
+     *
+     * @param maxBytesToPurge the desired number of bytes to be purged.
+     * @param preferScratchResources If true scratch resources will be purged prior to other
+     *                               resource types.
+     */
+    void purgeUnlockedResources(size_t bytesToPurge, bool preferScratchResources);
+
     /** Access the context capabilities */
     const GrCaps* caps() const { return fCaps; }
 
diff --git a/src/core/SkTDPQueue.h b/src/core/SkTDPQueue.h
index b5c9923..5f6fd3b 100644
--- a/src/core/SkTDPQueue.h
+++ b/src/core/SkTDPQueue.h
@@ -9,6 +9,7 @@
 #define SkTDPQueue_DEFINED
 
 #include "SkTDArray.h"
+#include "SkTSort.h"
 
 /**
  * This class implements a priority queue. T is the type of the elements in the queue. LESS is a
@@ -102,6 +103,19 @@
         to peek(). Otherwise, there is no guarantee about ordering of elements in the queue. */
     T at(int i) const { return fArray[i]; }
 
+    /** Sorts the queue into priority order.  The queue is only guarenteed to remain in sorted order
+     *  until any other operation, other than at(), is performed.
+     */
+    void sort() {
+        if (fArray.count() > 1) {
+            SkTQSort<T>(fArray.begin(), fArray.end() - 1, LESS);
+            for (int i = 0; i < fArray.count(); i++) {
+                this->setIndex(i);
+            }
+            this->validate();
+        }
+    }
+
 private:
     static int LeftOf(int x) { SkASSERT(x >= 0); return 2 * x + 1; }
     static int ParentOf(int x) { SkASSERT(x > 0); return (x - 1) >> 1; }
diff --git a/src/gpu/GrContext.cpp b/src/gpu/GrContext.cpp
index e7aff35..e78dc6a 100644
--- a/src/gpu/GrContext.cpp
+++ b/src/gpu/GrContext.cpp
@@ -204,6 +204,11 @@
     fResourceCache->purgeResourcesNotUsedSince(GrStdSteadyClock::now() - ms);
 }
 
+void GrContext::purgeUnlockedResources(size_t bytesToPurge, bool preferScratchResources) {
+    ASSERT_SINGLE_OWNER
+    fResourceCache->purgeUnlockedResources(bytesToPurge, preferScratchResources);
+}
+
 void GrContext::getResourceCacheUsage(int* resourceCount, size_t* resourceBytes) const {
     ASSERT_SINGLE_OWNER
 
diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp
index 53a62c0..ef703e2 100644
--- a/src/gpu/GrResourceCache.cpp
+++ b/src/gpu/GrResourceCache.cpp
@@ -536,6 +536,47 @@
     }
 }
 
+void GrResourceCache::purgeUnlockedResources(size_t bytesToPurge, bool preferScratchResources) {
+
+    const size_t tmpByteBudget = SkTMax((size_t)0, fBytes - bytesToPurge);
+    bool stillOverbudget = tmpByteBudget < fBytes;
+
+    if (preferScratchResources && bytesToPurge < fPurgeableBytes) {
+        // Sort the queue
+        fPurgeableQueue.sort();
+
+        // Make a list of the scratch resources to delete
+        SkTDArray<GrGpuResource*> scratchResources;
+        size_t scratchByteCount = 0;
+        for (int i = 0; i < fPurgeableQueue.count() && stillOverbudget; i++) {
+            GrGpuResource* resource = fPurgeableQueue.at(i);
+            SkASSERT(resource->isPurgeable());
+            if (!resource->getUniqueKey().isValid()) {
+                *scratchResources.append() = resource;
+                scratchByteCount += resource->gpuMemorySize();
+                stillOverbudget = tmpByteBudget < fBytes - scratchByteCount;
+            }
+        }
+
+        // Delete the scratch resources. This must be done as a separate pass
+        // to avoid messing up the sorted order of the queue
+        for (int i = 0; i < scratchResources.count(); i++) {
+            scratchResources.getAt(i)->cacheAccess().release();
+        }
+        stillOverbudget = tmpByteBudget < fBytes;
+
+        this->validate();
+    }
+
+    // Purge any remaining resources in LRU order
+    if (stillOverbudget) {
+        const size_t cachedByteCount = fMaxBytes;
+        fMaxBytes = tmpByteBudget;
+        this->purgeAsNeeded();
+        fMaxBytes = cachedByteCount;
+    }
+}
+
 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 6362c37..f851ab1 100644
--- a/src/gpu/GrResourceCache.h
+++ b/src/gpu/GrResourceCache.h
@@ -173,6 +173,18 @@
     /** Purge all resources not used since the passed in time. */
     void purgeResourcesNotUsedSince(GrStdSteadyClock::time_point);
 
+    /**
+     * Purge unlocked resources from the cache until the the provided byte count has been reached
+     * or we have purged all unlocked resources. The default policy is to purge in LRU order, but
+     * can be overridden to prefer purging scratch resources (in LRU order) prior to purging other
+     * resource types.
+     *
+     * @param maxBytesToPurge the desired number of bytes to be purged.
+     * @param preferScratchResources If true scratch resources will be purged prior to other
+     *                               resource types.
+     */
+    void purgeUnlockedResources(size_t bytesToPurge, bool preferScratchResources);
+
     /** 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 3a63521..08f60ea 100644
--- a/tests/ResourceCacheTest.cpp
+++ b/tests/ResourceCacheTest.cpp
@@ -1360,6 +1360,114 @@
     }
 }
 
+static void test_partial_purge(skiatest::Reporter* reporter) {
+    Mock mock(6, 100);
+    GrContext* context = mock.context();
+    GrResourceCache* cache = mock.cache();
+
+    enum TestsCase {
+        kOnlyScratch_TestCase = 0,
+        kPartialScratch_TestCase = 1,
+        kAllScratch_TestCase = 2,
+        kPartial_TestCase = 3,
+        kAll_TestCase = 4,
+        kNone_TestCase = 5,
+        kEndTests_TestCase = kNone_TestCase + 1
+    };
+
+    for (int testCase = 0; testCase < kEndTests_TestCase; testCase++) {
+
+        GrUniqueKey key1, key2, key3;
+        make_unique_key<0>(&key1, 1);
+        make_unique_key<0>(&key2, 2);
+        make_unique_key<0>(&key3, 3);
+
+        // Add three unique resources to the cache.
+        TestResource *unique1 = new TestResource(context->getGpu());
+        TestResource *unique2 = new TestResource(context->getGpu());
+        TestResource *unique3 = new TestResource(context->getGpu());
+
+        unique1->resourcePriv().setUniqueKey(key1);
+        unique2->resourcePriv().setUniqueKey(key2);
+        unique3->resourcePriv().setUniqueKey(key3);
+
+        unique1->setSize(10);
+        unique2->setSize(11);
+        unique3->setSize(12);
+
+        // Add two scratch resources to the cache.
+        TestResource *scratch1 = TestResource::CreateScratch(context->getGpu(), SkBudgeted::kYes,
+                                                             TestResource::kA_SimulatedProperty);
+        TestResource *scratch2 = TestResource::CreateScratch(context->getGpu(), SkBudgeted::kYes,
+                                                             TestResource::kB_SimulatedProperty);
+        scratch1->setSize(13);
+        scratch2->setSize(14);
+
+
+        REPORTER_ASSERT(reporter, 5 == cache->getBudgetedResourceCount());
+        REPORTER_ASSERT(reporter, 60 == cache->getBudgetedResourceBytes());
+        REPORTER_ASSERT(reporter, 0 == cache->getPurgeableBytes());
+
+        // Add resources to the purgeable queue
+        unique1->unref();
+        scratch1->unref();
+        unique2->unref();
+        scratch2->unref();
+        unique3->unref();
+
+        REPORTER_ASSERT(reporter, 5 == cache->getBudgetedResourceCount());
+        REPORTER_ASSERT(reporter, 60 == cache->getBudgetedResourceBytes());
+        REPORTER_ASSERT(reporter, 60 == cache->getPurgeableBytes());
+
+        switch(testCase) {
+            case kOnlyScratch_TestCase: {
+                context->purgeUnlockedResources(14, true);
+                REPORTER_ASSERT(reporter, 3 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 33 == cache->getBudgetedResourceBytes());
+                break;
+            }
+            case kPartialScratch_TestCase: {
+                context->purgeUnlockedResources(3, true);
+                REPORTER_ASSERT(reporter, 4 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 47 == cache->getBudgetedResourceBytes());
+                break;
+            }
+            case kAllScratch_TestCase: {
+                context->purgeUnlockedResources(50, true);
+                REPORTER_ASSERT(reporter, 0 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 0 == cache->getBudgetedResourceBytes());
+                break;
+            }
+            case kPartial_TestCase: {
+                context->purgeUnlockedResources(13, false);
+                REPORTER_ASSERT(reporter, 3 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 37 == cache->getBudgetedResourceBytes());
+                break;
+            }
+            case kAll_TestCase: {
+                context->purgeUnlockedResources(50, false);
+                REPORTER_ASSERT(reporter, 0 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 0 == cache->getBudgetedResourceBytes());
+                break;
+            }
+            case kNone_TestCase: {
+                context->purgeUnlockedResources(0, true);
+                context->purgeUnlockedResources(0, false);
+                REPORTER_ASSERT(reporter, 5 == cache->getBudgetedResourceCount());
+                REPORTER_ASSERT(reporter, 60 == cache->getBudgetedResourceBytes());
+                REPORTER_ASSERT(reporter, 60 == cache->getPurgeableBytes());
+                break;
+            }
+        };
+
+        // ensure all are purged before the next
+        context->purgeAllUnlockedResources();
+        REPORTER_ASSERT(reporter, 0 == cache->getBudgetedResourceCount());
+        REPORTER_ASSERT(reporter, 0 == cache->getPurgeableBytes());
+
+    }
+}
+
 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
@@ -1515,6 +1623,7 @@
     test_timestamp_wrap(reporter);
     test_flush(reporter);
     test_time_purge(reporter);
+    test_partial_purge(reporter);
     test_large_resource_count(reporter);
     test_custom_data(reporter);
     test_abandoned(reporter);
diff --git a/tests/TDPQueueTest.cpp b/tests/TDPQueueTest.cpp
index 70367a7..75eab25 100644
--- a/tests/TDPQueueTest.cpp
+++ b/tests/TDPQueueTest.cpp
@@ -150,7 +150,56 @@
    }
 }
 
+void sort_test(skiatest::Reporter* reporter) {
+    SkRandom random;
+
+    SkTDPQueue<Dummy *, Dummy::LessP, Dummy::PQIndex> pqTest;
+    SkTDPQueue<Dummy *, Dummy::LessP, Dummy::PQIndex> pqControl;
+
+    // Create a random set of Dummy objects and populate the test queue.
+    int count = random.nextULessThan(100);
+    SkTDArray<Dummy> testArray;
+    testArray.setReserve(count);
+    for (int i = 0; i < count; i++) {
+        Dummy *dummy = testArray.append();
+        dummy->fPriority = random.nextS();
+        dummy->fValue = random.nextS();
+        dummy->fIndex = -1;
+        pqTest.insert(&testArray[i]);
+    }
+
+    // Stick equivalent dummy objects into the control queue.
+    SkTDArray<Dummy> controlArray;
+    controlArray.setReserve(count);
+    for (int i = 0; i < count; i++) {
+        Dummy *dummy = controlArray.append();
+        dummy->fPriority = testArray[i].fPriority;
+        dummy->fValue = testArray[i].fValue;
+        dummy->fIndex = -1;
+        pqControl.insert(&controlArray[i]);
+    }
+
+    // Sort the queue
+    pqTest.sort();
+
+    // Compare elements in the queue to ensure they are in sorted order
+    int prevPriority = pqTest.peek()->fPriority;
+    for (int i = 0; i < count; i++) {
+        REPORTER_ASSERT(reporter, i <= pqTest.at(i)->fIndex);
+        REPORTER_ASSERT(reporter, prevPriority <= pqTest.at(i)->fPriority);
+        prevPriority = pqTest.at(i)->fPriority;
+    }
+
+    // Verify that after sorting the queue still produces the same result as the control queue
+    for (int i = 0; i < count; i++) {
+        REPORTER_ASSERT(reporter, *pqControl.peek() == *pqTest.peek());
+        pqControl.pop();
+        pqTest.pop();
+    }
+}
+
 DEF_TEST(TDPQueueTest, reporter) {
     simple_test(reporter);
     random_test(reporter);
+    sort_test(reporter);
 }