Increase testing of GrThreadSafeUniquelyKeyedProxyViewCache wrt GrResourceCache purging

Bug: 1108408
Change-Id: I2d6f89ce71b8d94be22b1ff38b9e205df94ca765
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/318205
Commit-Queue: Robert Phillips <robertphillips@google.com>
Reviewed-by: Adlai Holler <adlai@google.com>
diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp
index fcdd8d9..7c5a640 100644
--- a/src/gpu/GrResourceCache.cpp
+++ b/src/gpu/GrResourceCache.cpp
@@ -530,7 +530,7 @@
     }
 
     if (stillOverbudget) {
-        fThreadSafeViewCache->dropAllUniqueRefs(); // Is there a less nuclear option?
+        fThreadSafeViewCache->dropAllUniqueRefs(this);
 
         while (stillOverbudget && fPurgeableQueue.count()) {
             GrGpuResource* resource = fPurgeableQueue.peek();
@@ -546,7 +546,7 @@
 void GrResourceCache::purgeUnlockedResources(bool scratchResourcesOnly) {
     // In the scratch-only mode we could just drop the ones that are uniquely held and would
     // be converted to scratch textures
-    fThreadSafeViewCache->dropAllUniqueRefs();
+    fThreadSafeViewCache->dropAllUniqueRefs(nullptr);
 
     if (!scratchResourcesOnly) {
         // We could disable maintaining the heap property here, but it would add a lot of
diff --git a/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.cpp b/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.cpp
index 4a40198..1732677 100644
--- a/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.cpp
+++ b/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.cpp
@@ -7,6 +7,8 @@
 
 #include "src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h"
 
+#include "src/gpu/GrResourceCache.h"
+
 GrThreadSafeUniquelyKeyedProxyViewCache::GrThreadSafeUniquelyKeyedProxyViewCache()
     : fFreeEntryList(nullptr) {
 }
@@ -40,21 +42,26 @@
     // TODO: should we empty out the fFreeEntryList and reset fEntryAllocator?
 }
 
-void GrThreadSafeUniquelyKeyedProxyViewCache::dropAllUniqueRefs() {
+void GrThreadSafeUniquelyKeyedProxyViewCache::dropAllUniqueRefs(GrResourceCache* resourceCache) {
     SkAutoSpinlock lock{fSpinLock};
 
-    Entry* cur = fUniquelyKeyedProxyViewList.head();
-    Entry* next = cur ? cur->fNext : nullptr;
+    // Iterate from LRU to MRU
+    Entry* cur = fUniquelyKeyedProxyViewList.tail();
+    Entry* prev = cur ? cur->fPrev : nullptr;
 
     while (cur) {
+        if (resourceCache && !resourceCache->overBudget()) {
+            return;
+        }
+
         if (cur->fView.proxy()->unique()) {
             fUniquelyKeyedProxyViewMap.remove(cur->fKey);
             fUniquelyKeyedProxyViewList.remove(cur);
             this->recycleEntry(cur);
         }
 
-        cur = next;
-        next = cur ? cur->fNext : nullptr;
+        cur = prev;
+        prev = cur ? cur->fPrev : nullptr;
     }
 }
 
diff --git a/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h b/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h
index a82b13a..f692505 100644
--- a/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h
+++ b/src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h
@@ -49,7 +49,7 @@
 #endif
 
     void dropAllRefs()  SK_EXCLUDES(fSpinLock);
-    void dropAllUniqueRefs()  SK_EXCLUDES(fSpinLock);
+    void dropAllUniqueRefs(GrResourceCache* resourceCache)  SK_EXCLUDES(fSpinLock);
 
     GrSurfaceProxyView find(const GrUniqueKey&)  SK_EXCLUDES(fSpinLock);
 
diff --git a/tests/GrThreadSafeViewCacheTest.cpp b/tests/GrThreadSafeViewCacheTest.cpp
index 97b0695..2420323 100644
--- a/tests/GrThreadSafeViewCacheTest.cpp
+++ b/tests/GrThreadSafeViewCacheTest.cpp
@@ -82,13 +82,11 @@
         fRecorder1 = std::make_unique<SkDeferredDisplayListRecorder>(characterization);
 
         fRecorder2 = std::make_unique<SkDeferredDisplayListRecorder>(characterization);
-
-        SkBitmap tmp = create_up_arrow_bitmap(kImageWH);
-        SkAssertResult(CreateBackendTexture(fDContext, &fBETex, tmp));
     }
 
     ~TestHelper() {
-        DeleteBackendTexture(fDContext, fBETex);
+        fDContext->flush();
+        fDContext->submit(true);
     }
 
     Stats* stats() { return &fStats; }
@@ -134,7 +132,7 @@
         GrRecordingContext* rContext = canvas->recordingContext();
 
         auto view = AccessCachedView(rContext, this->threadSafeViewCache(),
-                                     fBETex, wh, failLookup, &fStats);
+                                     wh, failLookup, &fStats);
         SkASSERT(view);
 
         auto rtc = canvas->internal_private_accessTopLayerRenderTargetContext();
@@ -159,6 +157,8 @@
     // method also validates that the unique key doesn't appear in any of the other caches.
     bool checkView(SkCanvas* canvas, int wh, int hits, int misses, int numRefs) {
         if (fStats.fCacheHits != hits || fStats.fCacheMisses != misses) {
+            SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n",
+                     hits, fStats.fCacheHits, misses, fStats.fCacheMisses);
             return false;
         }
 
@@ -168,6 +168,9 @@
         auto threadSafeViewCache = this->threadSafeViewCache();
 
         GrSurfaceProxyView view = threadSafeViewCache->find(key);
+        if (!view.proxy()) {
+            return false;
+        }
 
         if (!view.proxy()->refCntGreaterThan(numRefs+1) ||  // +1 for 'view's ref
             view.proxy()->refCntGreaterThan(numRefs+2)) {
@@ -206,13 +209,21 @@
         return true;
     }
 
+    size_t gpuSize(int wh) const {
+        GrBackendFormat format = fDContext->defaultBackendFormat(kRGBA_8888_SkColorType,
+                                                                 GrRenderable::kNo);
+
+        return GrSurface::ComputeSize(*fDContext->priv().caps(), format,
+                                      {wh, wh}, 1, GrMipMapped::kNo, false);
+    }
+
 private:
     static GrSurfaceProxyView AccessCachedView(GrRecordingContext*,
                                                GrThreadSafeUniquelyKeyedProxyViewCache*,
-                                               GrBackendTexture, int wh,
+                                               int wh,
                                                bool failLookup, Stats*);
     static GrSurfaceProxyView CreateViewOnCpu(GrRecordingContext*, int wh, Stats*);
-    static GrSurfaceProxyView CreateViewOnGpu(GrDirectContext*, GrBackendTexture, int wh, Stats*);
+    static GrSurfaceProxyView CreateViewOnGpu(GrDirectContext*, int wh, Stats*);
 
     Stats fStats;
     GrDirectContext* fDContext = nullptr;
@@ -220,8 +231,6 @@
     sk_sp<SkSurface> fDst;
     std::unique_ptr<SkDeferredDisplayListRecorder> fRecorder1;
     std::unique_ptr<SkDeferredDisplayListRecorder> fRecorder2;
-
-    GrBackendTexture fBETex;
 };
 
 GrSurfaceProxyView TestHelper::CreateViewOnCpu(GrRecordingContext* rContext,
@@ -244,17 +253,15 @@
 }
 
 GrSurfaceProxyView TestHelper::CreateViewOnGpu(GrDirectContext* dContext,
-                                               GrBackendTexture beTex,
                                                int wh,
                                                Stats* stats) {
-    SkASSERT(!stats->fNumHWCreations); // we had better not do this more than once
-
     GrProxyProvider* proxyProvider = dContext->priv().proxyProvider();
 
-    sk_sp<GrTextureProxy> proxy = proxyProvider->wrapBackendTexture(beTex,
-                                                                    kBorrow_GrWrapOwnership,
-                                                                    GrWrapCacheable::kNo,
-                                                                    kRead_GrIOType);
+    sk_sp<GrTextureProxy> proxy = proxyProvider->createProxyFromBitmap(create_up_arrow_bitmap(wh),
+                                                                       GrMipmapped::kNo,
+                                                                       SkBackingFit::kExact,
+                                                                       SkBudgeted::kYes);
+
     GrSwizzle swizzle = dContext->priv().caps()->getReadSwizzle(proxy->backendFormat(),
                                                                 GrColorType::kRGBA_8888);
     ++stats->fNumHWCreations;
@@ -266,7 +273,6 @@
 GrSurfaceProxyView TestHelper::AccessCachedView(
                                     GrRecordingContext* rContext,
                                     GrThreadSafeUniquelyKeyedProxyViewCache* threadSafeViewCache,
-                                    GrBackendTexture beTex,
                                     int wh,
                                     bool failLookup,
                                     Stats* stats) {
@@ -283,7 +289,7 @@
 
     GrSurfaceProxyView view;
     if (GrDirectContext* dContext = rContext->asDirectContext()) {
-        view = CreateViewOnGpu(dContext, beTex, wh, stats);
+        view = CreateViewOnGpu(dContext, wh, stats);
     } else {
         view = CreateViewOnCpu(rContext, wh, stats);
     }
@@ -297,10 +303,12 @@
     TestHelper helper(ctxInfo.directContext());
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
-    helper.checkView(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
-    helper.checkView(helper.ddlCanvas2(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
     REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
@@ -312,13 +320,16 @@
     TestHelper helper(ctxInfo.directContext());
 
     helper.accessCachedView(helper.liveCanvas(), kImageWH);
-    helper.checkView(helper.liveCanvas(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
-    helper.checkView(helper.ddlCanvas1(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
 
     helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
-    helper.checkView(helper.ddlCanvas2(), kImageWH, /*hits*/ 2, /*misses*/ 1, /*refs*/ 3);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
+                                               /*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
     REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
@@ -330,10 +341,12 @@
     TestHelper helper(ctxInfo.directContext());
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
-    helper.checkView(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     helper.accessCachedView(helper.liveCanvas(), kImageWH);
-    helper.checkView(helper.liveCanvas(), kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
     REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
@@ -345,11 +358,13 @@
     TestHelper helper(ctxInfo.directContext());
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
-    helper.checkView(helper.ddlCanvas1(), kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     static const bool kFailLookup = true;
     helper.accessCachedView(helper.ddlCanvas2(), kImageWH, kFailLookup);
-    helper.checkView(helper.ddlCanvas2(), kImageWH, /*hits*/ 0, /*misses*/ 2, /*refs*/ 2);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 2, /*refs*/ 2));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
     REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
@@ -379,51 +394,190 @@
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
     sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
-    helper.checkView(nullptr, kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
     sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
-    helper.checkView(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 2);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
 
     ddl1 = nullptr;
-    helper.checkView(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 1));
 
     ddl2 = nullptr;
-    helper.checkView(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 0);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 0));
 
     // The cache still has its ref
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
 
-    helper.checkView(nullptr, kImageWH, /*hits*/ 1, /*misses*/ 1, /*refs*/ 0);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 0));
 }
 
-// Case 7: check that dropAllRefs() and dropAllUniqueRefs work as expected
+// Case 7: check that invoking dropAllRefs and dropAllUniqueRefs directly works as expected
 DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache7, reporter, ctxInfo) {
     TestHelper helper(ctxInfo.directContext());
 
     helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
     sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
-    helper.checkView(nullptr, kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
 
     helper.accessCachedView(helper.ddlCanvas2(), 2*kImageWH);
     sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
-    helper.checkView(nullptr, 2*kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, 2*kImageWH,
+                                               /*hits*/ 0, /*misses*/ 2, /*refs*/ 1));
 
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
 
-    helper.threadSafeViewCache()->dropAllUniqueRefs();
+    helper.threadSafeViewCache()->dropAllUniqueRefs(nullptr);
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
 
     ddl1 = nullptr;
 
-    helper.threadSafeViewCache()->dropAllUniqueRefs();
+    helper.threadSafeViewCache()->dropAllUniqueRefs(nullptr);
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
-    helper.checkView(nullptr, 2*kImageWH, /*hits*/ 0, /*misses*/ 1, /*refs*/ 1);
+    REPORTER_ASSERT(reporter, helper.checkView(nullptr, 2*kImageWH,
+                                               /*hits*/ 0, /*misses*/ 2, /*refs*/ 1));
 
     helper.threadSafeViewCache()->dropAllRefs();
     REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
 
     ddl2 = nullptr;
 }
+
+// Case 8: This checks that GrContext::abandonContext works as expected wrt the thread
+//         safe cache. This simulates the case where we have one DDL that has finished
+//         recording but one still recording when the abandonContext fires.
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache8, reporter, ctxInfo) {
+    TestHelper helper(ctxInfo.directContext());
+
+    helper.accessCachedView(helper.liveCanvas(), kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
+
+    helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
+    sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
+
+    helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
+                                               /*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
+    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
+    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
+
+    ctxInfo.directContext()->abandonContext(); // This should exercise dropAllRefs
+
+    sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
+
+    ddl1 = nullptr;
+    ddl2 = nullptr;
+}
+
+// Case 9: This checks that GrContext::releaseResourcesAndAbandonContext works as expected wrt
+//         the thread safe cache. This simulates the case where we have one DDL that has finished
+//         recording but one still recording when the releaseResourcesAndAbandonContext fires.
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache9, reporter, ctxInfo) {
+    TestHelper helper(ctxInfo.directContext());
+
+    helper.accessCachedView(helper.liveCanvas(), kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
+
+    helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
+    sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
+
+    helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
+                                               /*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
+    REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
+    REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
+
+    ctxInfo.directContext()->releaseResourcesAndAbandonContext(); // This should hit dropAllRefs
+
+    sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
+
+    ddl1 = nullptr;
+    ddl2 = nullptr;
+}
+
+// Case 10: This checks that the GrContext::purgeUnlockedResources(size_t) variant works as
+//          expected wrt the thread safe cache. It, in particular, tests out the MRU behavior
+//          of the shared cache.
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache10, reporter, ctxInfo) {
+    auto dContext = ctxInfo.directContext();
+
+    if (GrBackendApi::kOpenGL != dContext->backend()) {
+        // The lower-level backends have too much going on for the following simple purging
+        // test to work
+        return;
+    }
+
+    TestHelper helper(dContext);
+
+    helper.accessCachedView(helper.liveCanvas(), kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
+
+    helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
+    sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
+                                               /*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
+
+    helper.accessCachedView(helper.liveCanvas(), 2*kImageWH);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
+                                               /*hits*/ 1, /*misses*/ 2, /*refs*/ 1));
+
+    helper.accessCachedView(helper.ddlCanvas2(), 2*kImageWH);
+    sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
+    REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), 2*kImageWH,
+                                               /*hits*/ 2, /*misses*/ 2, /*refs*/ 2));
+
+    dContext->flush();
+    dContext->submit(true);
+
+    // This should clear out everything but the textures locked in the thread-safe cache
+    dContext->purgeUnlockedResources(false);
+
+    ddl1 = nullptr;
+    ddl2 = nullptr;
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
+                                               /*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
+                                               /*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
+
+    // Regardless of which image is MRU, this should force the other out
+    size_t desiredBytes = helper.gpuSize(2*kImageWH) + helper.gpuSize(kImageWH)/2;
+
+    auto cache = dContext->priv().getResourceCache();
+    size_t currentBytes = cache->getResourceBytes();
+
+    SkASSERT(currentBytes >= desiredBytes);
+    size_t amountToPurge = currentBytes - desiredBytes;
+
+    // The 2*kImageWH texture should be MRU.
+    dContext->purgeUnlockedResources(amountToPurge, true);
+
+    REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
+
+    REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
+                                               /*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
+}