/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.imagepipeline.cache; import android.graphics.Bitmap; import android.os.SystemClock; import com.facebook.common.internal.Supplier; import com.facebook.common.memory.MemoryTrimType; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.ResourceReleaser; import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory; import com.android.internal.util.Predicate; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @PrepareForTest({SystemClock.class}) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) @Config(manifest=Config.NONE) public class CountingMemoryCacheTest { private static final int CACHE_MAX_SIZE = 1200; private static final int CACHE_MAX_COUNT = 4; private static final int CACHE_EVICTION_QUEUE_MAX_SIZE = 1100; private static final int CACHE_EVICTION_QUEUE_MAX_COUNT = 3; private static final int CACHE_ENTRY_MAX_SIZE = 1000; @Mock public ResourceReleaser<Integer> mReleaser; @Mock public CountingMemoryCache.CacheTrimStrategy mCacheTrimStrategy; @Mock public Supplier<MemoryCacheParams> mParamsSupplier; @Mock public CountingMemoryCache.EntryStateObserver<String> mEntryStateObserver; @Mock public PlatformBitmapFactory.BitmapCreationObserver mBitmapCreationObserver; @Mock public Bitmap mBitmap; @Rule public PowerMockRule rule = new PowerMockRule(); private ValueDescriptor<Integer> mValueDescriptor; private MemoryCacheParams mParams; private CountingMemoryCache<String, Integer> mCache; private PlatformBitmapFactory mPlatformBitmapFactory; private CloseableReference<Bitmap> mBitmapReference; private static final String KEY = "KEY"; private static final String[] KEYS = new String[] {"k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"}; private static final ResourceReleaser<Bitmap> FAKE_BITMAP_RESOURCE_RELEASER = new ResourceReleaser<Bitmap>() { @Override public void release(Bitmap value) { } }; @Before public void setUp() { MockitoAnnotations.initMocks(this); PowerMockito.mockStatic(SystemClock.class); PowerMockito.when(SystemClock.uptimeMillis()).thenReturn(0L); mValueDescriptor = new ValueDescriptor<Integer>() { @Override public int getSizeInBytes(Integer value) { return value; } }; mParams = new MemoryCacheParams( CACHE_MAX_SIZE, CACHE_MAX_COUNT, CACHE_EVICTION_QUEUE_MAX_SIZE, CACHE_EVICTION_QUEUE_MAX_COUNT, CACHE_ENTRY_MAX_SIZE); when(mParamsSupplier.get()).thenReturn(mParams); mPlatformBitmapFactory = Mockito.mock(PlatformBitmapFactory.class); mBitmapReference = CloseableReference.of(mBitmap, FAKE_BITMAP_RESOURCE_RELEASER); mCache = new CountingMemoryCache<>( mValueDescriptor, mCacheTrimStrategy, mParamsSupplier, mPlatformBitmapFactory, true); } @Test public void testSetCreationListener() throws Exception { verify(mPlatformBitmapFactory, times(1)) .setCreationListener(any(PlatformBitmapFactory.BitmapCreationObserver.class)); } @Test public void testAddBitmapReference() throws Exception { when(mPlatformBitmapFactory.createBitmapInternal(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenReturn(mBitmapReference); when(mPlatformBitmapFactory.createBitmap(anyInt(), anyInt())).thenCallRealMethod(); when(mPlatformBitmapFactory.createBitmap(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenCallRealMethod(); when(mPlatformBitmapFactory.createBitmap( anyInt(), anyInt(), any(Bitmap.Config.class), anyObject())) .thenCallRealMethod(); CloseableReference<Bitmap> bitmapReference = mPlatformBitmapFactory.createBitmap(50, 50); assertEquals(bitmapReference, mBitmapReference); verify(mPlatformBitmapFactory).addBitmapReference(mBitmapReference.get(), null); } @Test public void testOnBitmapCreated() throws Exception { mPlatformBitmapFactory = mock(PlatformBitmapFactory.class, CALLS_REAL_METHODS); mCache = new CountingMemoryCache<>( mValueDescriptor, mCacheTrimStrategy, mParamsSupplier, mPlatformBitmapFactory, true); assertEquals("other entries count mismatch", 0, mCache.mOtherEntries.size()); mPlatformBitmapFactory.addBitmapReference(mBitmapReference.get(), null); assertEquals("other entries count mismatch" ,1, mCache.mOtherEntries.size()); } @Test public void testCache() { mCache.cache(KEY, newReference(100)); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 1); verify(mReleaser, never()).release(anyInt()); } @Test public void testClosingOriginalReference() { CloseableReference<Integer> originalRef = newReference(100); mCache.cache(KEY, originalRef); // cache should make its own copy and closing the original reference after caching // should not affect the cached value originalRef.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 1); verify(mReleaser, never()).release(anyInt()); } @Test public void testClosingClientReference() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100)); // cached item should get exclusively owned cachedRef.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); assertExclusivelyOwned(KEY, 100); verify(mReleaser, never()).release(anyInt()); } @Test public void testNotExclusiveAtFirst() { mCache.cache(KEY, newReference(100), mEntryStateObserver); verify(mEntryStateObserver, never()).onExclusivityChanged(anyString(), anyBoolean()); } @Test public void testToggleExclusive() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100), mEntryStateObserver); cachedRef.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); mCache.get(KEY); verify(mEntryStateObserver).onExclusivityChanged(KEY, false); } @Test public void testCantReuseNonExclusive() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100), mEntryStateObserver); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); CloseableReference<Integer> reusedRef = mCache.reuse(KEY); assertNull(reusedRef); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); verify(mEntryStateObserver, never()).onExclusivityChanged(anyString(), anyBoolean()); cachedRef.close(); } @Test public void testCanReuseExclusive() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100), mEntryStateObserver); cachedRef.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); cachedRef = mCache.reuse(KEY); assertNotNull(cachedRef); verify(mEntryStateObserver).onExclusivityChanged(KEY, false); assertTotalSize(0, 0); assertExclusivelyOwnedSize(0, 0); cachedRef.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); } @Test public void testReuseExclusive_CacheSameItem() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100), mEntryStateObserver); cachedRef.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); cachedRef = mCache.reuse(KEY); assertNotNull(cachedRef); verify(mEntryStateObserver).onExclusivityChanged(KEY, false); assertTotalSize(0, 0); assertExclusivelyOwnedSize(0, 0); CloseableReference<Integer> newItem = mCache.cache(KEY, cachedRef); cachedRef.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); newItem.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); } @Test public void testReuseExclusive_CacheSameItemWithDifferentKey() { CloseableReference<Integer> cachedRef = mCache.cache(KEY, newReference(100), mEntryStateObserver); cachedRef.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); cachedRef = mCache.reuse(KEY); assertNotNull(cachedRef); verify(mEntryStateObserver).onExclusivityChanged(KEY, false); assertTotalSize(0, 0); assertExclusivelyOwnedSize(0, 0); CloseableReference<Integer> newItem = mCache.cache(KEYS[2], cachedRef); cachedRef.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); newItem.close(); verify(mEntryStateObserver).onExclusivityChanged(KEY, true); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); } @Test public void testInUseCount() { CloseableReference<Integer> cachedRef1 = mCache.cache(KEY, newReference(100)); CloseableReference<Integer> cachedRef2a = mCache.get(KEY); CloseableReference<Integer> cachedRef2b = cachedRef2a.clone(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 2); CloseableReference<Integer> cachedRef3a = mCache.get(KEY); CloseableReference<Integer> cachedRef3b = cachedRef3a.clone(); CloseableReference<Integer> cachedRef3c = cachedRef3b.clone(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 3); cachedRef1.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 2); // all copies of cachedRef2a need to be closed for usage count to drop cachedRef2a.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 2); cachedRef2b.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 1); // all copies of cachedRef3a need to be closed for usage count to drop cachedRef3c.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 1); cachedRef3b.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEY, 100, 1); cachedRef3a.close(); assertTotalSize(1, 100); assertExclusivelyOwnedSize(1, 100); assertExclusivelyOwned(KEY, 100); } @Test public void testCachingSameKeyTwice() { CloseableReference<Integer> originalRef1 = newReference(110); CloseableReference<Integer> cachedRef1 = mCache.cache(KEY, originalRef1); CloseableReference<Integer> cachedRef2a = mCache.get(KEY); CloseableReference<Integer> cachedRef2b = cachedRef2a.clone(); CloseableReference<Integer> cachedRef3 = mCache.get(KEY); CountingMemoryCache.Entry<String, Integer> entry1 = mCache.mCachedEntries.get(KEY); CloseableReference<Integer> cachedRef2 = mCache.cache(KEY, newReference(120)); CountingMemoryCache.Entry<String, Integer> entry2 = mCache.mCachedEntries.get(KEY); assertNotSame(entry1, entry2); assertOrphanWithCount(entry1, 3); assertSharedWithCount(KEY, 120, 1); // release the orphaned reference only when all clients are gone originalRef1.close(); cachedRef2b.close(); assertOrphanWithCount(entry1, 3); cachedRef2a.close(); assertOrphanWithCount(entry1, 2); cachedRef1.close(); assertOrphanWithCount(entry1, 1); verify(mReleaser, never()).release(anyInt()); cachedRef3.close(); assertOrphanWithCount(entry1, 0); verify(mReleaser).release(110); } @Test public void testDoesNotCacheBigValues() { assertNull(mCache.cache(KEY, newReference(CACHE_ENTRY_MAX_SIZE + 1))); } @Test public void testDoesCacheNotTooBigValues() { assertNotNull(mCache.cache(KEY, newReference(CACHE_ENTRY_MAX_SIZE))); } @Test public void testEviction_ByTotalSize() { // value 4 cannot fit the cache CloseableReference<Integer> originalRef1 = newReference(400); CloseableReference<Integer> valueRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); CloseableReference<Integer> originalRef2 = newReference(500); CloseableReference<Integer> valueRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); CloseableReference<Integer> originalRef3 = newReference(100); CloseableReference<Integer> valueRef3 = mCache.cache(KEYS[3], originalRef3); originalRef3.close(); CloseableReference<Integer> originalRef4 = newReference(700); CloseableReference<Integer> valueRef4 = mCache.cache(KEYS[4], originalRef4); originalRef4.close(); assertTotalSize(3, 1000); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEYS[1], 400, 1); assertSharedWithCount(KEYS[2], 500, 1); assertSharedWithCount(KEYS[3], 100, 1); assertNotCached(KEYS[4], 700); assertNull(valueRef4); // closing the clients of cached items will make them viable for eviction valueRef1.close(); valueRef2.close(); valueRef3.close(); assertTotalSize(3, 1000); assertExclusivelyOwnedSize(3, 1000); // value 4 can now fit after evicting value1 and value2 valueRef4 = mCache.cache(KEYS[4], newReference(700)); assertTotalSize(2, 800); assertExclusivelyOwnedSize(1, 100); assertNotCached(KEYS[1], 400); assertNotCached(KEYS[2], 500); assertExclusivelyOwned(KEYS[3], 100); assertSharedWithCount(KEYS[4], 700, 1); verify(mReleaser).release(400); verify(mReleaser).release(500); } @Test public void testEviction_ByTotalCount() { // value 5 cannot fit the cache CloseableReference<Integer> originalRef1 = newReference(110); CloseableReference<Integer> valueRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); CloseableReference<Integer> originalRef2 = newReference(120); CloseableReference<Integer> valueRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); CloseableReference<Integer> originalRef3 = newReference(130); CloseableReference<Integer> valueRef3 = mCache.cache(KEYS[3], originalRef3); originalRef3.close(); CloseableReference<Integer> originalRef4 = newReference(140); CloseableReference<Integer> valueRef4 = mCache.cache(KEYS[4], originalRef4); originalRef4.close(); CloseableReference<Integer> originalRef5 = newReference(150); CloseableReference<Integer> valueRef5 = mCache.cache(KEYS[5], originalRef5); originalRef5.close(); assertTotalSize(4, 500); assertExclusivelyOwnedSize(0, 0); assertSharedWithCount(KEYS[1], 110, 1); assertSharedWithCount(KEYS[2], 120, 1); assertSharedWithCount(KEYS[3], 130, 1); assertSharedWithCount(KEYS[4], 140, 1); assertNotCached(KEYS[5], 150); assertNull(valueRef5); // closing the clients of cached items will make them viable for eviction valueRef1.close(); valueRef2.close(); valueRef3.close(); assertTotalSize(4, 500); assertExclusivelyOwnedSize(3, 360); // value 4 can now fit after evicting value1 valueRef4 = mCache.cache(KEYS[5], newReference(150)); assertTotalSize(4, 540); assertExclusivelyOwnedSize(2, 250); assertNotCached(KEYS[1], 110); assertExclusivelyOwned(KEYS[2], 120); assertExclusivelyOwned(KEYS[3], 130); assertSharedWithCount(KEYS[4], 140, 1); assertSharedWithCount(KEYS[5], 150, 1); verify(mReleaser).release(110); } @Test public void testEviction_ByEvictionQueueSize() { CloseableReference<Integer> originalRef1 = newReference(200); CloseableReference<Integer> valueRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); valueRef1.close(); CloseableReference<Integer> originalRef2 = newReference(300); CloseableReference<Integer> valueRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); valueRef2.close(); CloseableReference<Integer> originalRef3 = newReference(700); CloseableReference<Integer> valueRef3 = mCache.cache(KEYS[3], originalRef3); originalRef3.close(); assertTotalSize(3, 1200); assertExclusivelyOwnedSize(2, 500); assertExclusivelyOwned(KEYS[1], 200); assertExclusivelyOwned(KEYS[2], 300); assertSharedWithCount(KEYS[3], 700, 1); verify(mReleaser, never()).release(anyInt()); // closing the client reference for item3 will cause item1 to be evicted valueRef3.close(); assertTotalSize(2, 1000); assertExclusivelyOwnedSize(2, 1000); assertNotCached(KEYS[1], 200); assertExclusivelyOwned(KEYS[2], 300); assertExclusivelyOwned(KEYS[3], 700); verify(mReleaser).release(200); } @Test public void testEviction_ByEvictionQueueCount() { CloseableReference<Integer> originalRef1 = newReference(110); CloseableReference<Integer> valueRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); valueRef1.close(); CloseableReference<Integer> originalRef2 = newReference(120); CloseableReference<Integer> valueRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); valueRef2.close(); CloseableReference<Integer> originalRef3 = newReference(130); CloseableReference<Integer> valueRef3 = mCache.cache(KEYS[3], originalRef3); originalRef3.close(); valueRef3.close(); CloseableReference<Integer> originalRef4 = newReference(140); CloseableReference<Integer> valueRef4 = mCache.cache(KEYS[4], originalRef4); originalRef4.close(); assertTotalSize(4, 500); assertExclusivelyOwnedSize(3, 360); assertExclusivelyOwned(KEYS[1], 110); assertExclusivelyOwned(KEYS[2], 120); assertExclusivelyOwned(KEYS[3], 130); assertSharedWithCount(KEYS[4], 140, 1); verify(mReleaser, never()).release(anyInt()); // closing the client reference for item4 will cause item1 to be evicted valueRef4.close(); assertTotalSize(3, 390); assertExclusivelyOwnedSize(3, 390); assertNotCached(KEYS[1], 110); assertExclusivelyOwned(KEYS[2], 120); assertExclusivelyOwned(KEYS[3], 130); assertExclusivelyOwned(KEYS[4], 140); verify(mReleaser).release(110); } @Test public void testUpdatesCacheParams() { InOrder inOrder = inOrder(mParamsSupplier); CloseableReference<Integer> originalRef = newReference(700); CloseableReference<Integer> cachedRef = mCache.cache(KEYS[2], originalRef); originalRef.close(); cachedRef.close(); mCache.get(KEY); inOrder.verify(mParamsSupplier).get(); PowerMockito.when(SystemClock.uptimeMillis()) .thenReturn(CountingMemoryCache.PARAMS_INTERCHECK_INTERVAL_MS - 1); mCache.get(KEY); inOrder.verify(mParamsSupplier, never()).get(); mCache.get(KEY); inOrder.verify(mParamsSupplier, never()).get(); assertTotalSize(1, 700); assertExclusivelyOwnedSize(1, 700); mParams = new MemoryCacheParams( 500 /* cache max size */, CACHE_MAX_COUNT, CACHE_EVICTION_QUEUE_MAX_SIZE, CACHE_EVICTION_QUEUE_MAX_COUNT, CACHE_ENTRY_MAX_SIZE); when(mParamsSupplier.get()).thenReturn(mParams); PowerMockito.when(SystemClock.uptimeMillis()) .thenReturn(CountingMemoryCache.PARAMS_INTERCHECK_INTERVAL_MS); mCache.get(KEY); inOrder.verify(mParamsSupplier).get(); assertTotalSize(0, 0); assertExclusivelyOwnedSize(0, 0); verify(mReleaser).release(700); } @Test public void testRemoveAllMatchingPredicate() { CloseableReference<Integer> originalRef1 = newReference(110); CloseableReference<Integer> valueRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); valueRef1.close(); CloseableReference<Integer> originalRef2 = newReference(120); CloseableReference<Integer> valueRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); valueRef2.close(); CloseableReference<Integer> originalRef3 = newReference(130); CloseableReference<Integer> valueRef3 = mCache.cache(KEYS[3], originalRef3); originalRef3.close(); CountingMemoryCache.Entry<String, Integer> entry3 = mCache.mCachedEntries.get(KEYS[3]); CloseableReference<Integer> originalRef4 = newReference(150); CloseableReference<Integer> valueRef4 = mCache.cache(KEYS[4], originalRef4); originalRef4.close(); int numEvictedEntries = mCache.removeAll( new Predicate<String>() { @Override public boolean apply(String key) { return key.equals(KEYS[2]) || key.equals(KEYS[3]); } }); assertEquals(2, numEvictedEntries); assertTotalSize(2, 260); assertExclusivelyOwnedSize(1, 110); assertExclusivelyOwned(KEYS[1], 110); assertNotCached(KEYS[2], 120); assertOrphanWithCount(entry3, 1); assertSharedWithCount(KEYS[4], 150, 1); verify(mReleaser).release(120); verify(mReleaser, never()).release(130); valueRef3.close(); verify(mReleaser).release(130); } @Test public void testClear() { CloseableReference<Integer> originalRef1 = newReference(110); CloseableReference<Integer> cachedRef1 = mCache.cache(KEYS[1], originalRef1); originalRef1.close(); CountingMemoryCache.Entry<String, Integer> entry1 = mCache.mCachedEntries.get(KEYS[1]); CloseableReference<Integer> originalRef2 = newReference(120); CloseableReference<Integer> cachedRef2 = mCache.cache(KEYS[2], originalRef2); originalRef2.close(); cachedRef2.close(); mCache.clear(); assertTotalSize(0, 0); assertExclusivelyOwnedSize(0, 0); assertOrphanWithCount(entry1, 1); assertNotCached(KEYS[2], 120); verify(mReleaser).release(120); cachedRef1.close(); verify(mReleaser).release(110); } @Test public void testTrimming() { MemoryTrimType memoryTrimType = MemoryTrimType.OnCloseToDalvikHeapLimit; mParams = new MemoryCacheParams(1100, 10, 1100, 10, 110); when(mParamsSupplier.get()).thenReturn(mParams); PowerMockito.when(SystemClock.uptimeMillis()) .thenReturn(CountingMemoryCache.PARAMS_INTERCHECK_INTERVAL_MS); InOrder inOrder = inOrder(mReleaser); // create original references CloseableReference<Integer>[] originalRefs = new CloseableReference[10]; for (int i = 0; i < 10; i++) { originalRefs[i] = newReference(100 + i); } // cache items & close the original references CloseableReference<Integer>[] cachedRefs = new CloseableReference[10]; for (int i = 0; i < 10; i++) { cachedRefs[i] = mCache.cache(KEYS[i], originalRefs[i]); originalRefs[i].close(); } // cache should keep alive the items until evicted inOrder.verify(mReleaser, never()).release(anyInt()); // trimming cannot evict shared entries when(mCacheTrimStrategy.getTrimRatio(memoryTrimType)).thenReturn(1.00); mCache.trim(memoryTrimType); assertSharedWithCount(KEYS[0], 100, 1); assertSharedWithCount(KEYS[1], 101, 1); assertSharedWithCount(KEYS[2], 102, 1); assertSharedWithCount(KEYS[3], 103, 1); assertSharedWithCount(KEYS[4], 104, 1); assertSharedWithCount(KEYS[5], 105, 1); assertSharedWithCount(KEYS[6], 106, 1); assertSharedWithCount(KEYS[7], 107, 1); assertSharedWithCount(KEYS[8], 108, 1); assertSharedWithCount(KEYS[9], 109, 1); assertTotalSize(10, 1045); assertExclusivelyOwnedSize(0, 0); // close 7 client references cachedRefs[8].close(); cachedRefs[2].close(); cachedRefs[7].close(); cachedRefs[3].close(); cachedRefs[6].close(); cachedRefs[4].close(); cachedRefs[5].close(); assertSharedWithCount(KEYS[0], 100, 1); assertSharedWithCount(KEYS[1], 101, 1); assertSharedWithCount(KEYS[9], 109, 1); assertExclusivelyOwned(KEYS[8], 108); assertExclusivelyOwned(KEYS[2], 102); assertExclusivelyOwned(KEYS[7], 107); assertExclusivelyOwned(KEYS[3], 103); assertExclusivelyOwned(KEYS[6], 106); assertExclusivelyOwned(KEYS[4], 104); assertExclusivelyOwned(KEYS[5], 105); assertTotalSize(10, 1045); assertExclusivelyOwnedSize(7, 735); // Trim cache by 45%. This means that out of total of 1045 bytes cached, 574 should remain. // 310 bytes is used by the clients, which leaves 264 for the exclusively owned items. // Only the two most recent exclusively owned items fit, and they occupy 209 bytes. when(mCacheTrimStrategy.getTrimRatio(memoryTrimType)).thenReturn(0.45); mCache.trim(memoryTrimType); assertSharedWithCount(KEYS[0], 100, 1); assertSharedWithCount(KEYS[1], 101, 1); assertSharedWithCount(KEYS[9], 109, 1); assertExclusivelyOwned(KEYS[4], 104); assertExclusivelyOwned(KEYS[5], 105); assertNotCached(KEYS[8], 108); assertNotCached(KEYS[2], 102); assertNotCached(KEYS[7], 107); assertNotCached(KEYS[3], 103); assertNotCached(KEYS[6], 106); assertTotalSize(5, 519); assertExclusivelyOwnedSize(2, 209); inOrder.verify(mReleaser).release(108); inOrder.verify(mReleaser).release(102); inOrder.verify(mReleaser).release(107); inOrder.verify(mReleaser).release(103); inOrder.verify(mReleaser).release(106); // Full trim. All exclusively owned items should be evicted. when(mCacheTrimStrategy.getTrimRatio(memoryTrimType)).thenReturn(1.00); mCache.trim(memoryTrimType); assertSharedWithCount(KEYS[0], 100, 1); assertSharedWithCount(KEYS[1], 101, 1); assertSharedWithCount(KEYS[9], 109, 1); assertNotCached(KEYS[8], 108); assertNotCached(KEYS[2], 102); assertNotCached(KEYS[7], 107); assertNotCached(KEYS[3], 103); assertNotCached(KEYS[6], 106); assertNotCached(KEYS[6], 104); assertNotCached(KEYS[6], 105); assertTotalSize(3, 310); assertExclusivelyOwnedSize(0, 0); inOrder.verify(mReleaser).release(104); inOrder.verify(mReleaser).release(105); } @Test public void testContains() { assertFalse(mCache.contains(KEY)); CloseableReference<Integer> newRef = mCache.cache(KEY, newReference(100)); assertTrue(mCache.contains(KEY)); assertFalse(mCache.contains(KEYS[0])); newRef.close(); assertTrue(mCache.contains(KEY)); assertFalse(mCache.contains(KEYS[0])); CloseableReference<Integer> reuse = mCache.reuse(KEY); reuse.close(); assertFalse(mCache.contains(KEY)); assertFalse(mCache.contains(KEYS[0])); } private CloseableReference<Integer> newReference(int size) { return CloseableReference.of(size, mReleaser); } private void assertSharedWithCount(String key, Integer value, int count) { assertTrue("key not found in the cache", mCache.mCachedEntries.contains(key)); assertFalse("key found in the exclusives", mCache.mExclusiveEntries.contains(key)); CountingMemoryCache.Entry<String, Integer> entry = mCache.mCachedEntries.get(key); assertNotNull("entry not found in the cache", entry); assertEquals("key mismatch", key, entry.key); assertEquals("value mismatch", value, entry.valueRef.get()); assertEquals("client count mismatch", count, entry.clientCount); assertFalse("entry is an orphan", entry.isOrphan); } private void assertExclusivelyOwned(String key, Integer value) { assertTrue("key not found in the cache", mCache.mCachedEntries.contains(key)); assertTrue("key not found in the exclusives", mCache.mExclusiveEntries.contains(key)); CountingMemoryCache.Entry<String, Integer> entry = mCache.mCachedEntries.get(key); assertNotNull("entry not found in the cache", entry); assertEquals("key mismatch", key, entry.key); assertEquals("value mismatch", value, entry.valueRef.get()); assertEquals("client count greater than zero", 0, entry.clientCount); assertFalse("entry is an orphan", entry.isOrphan); } private void assertNotCached(String key, Integer value) { assertFalse("key found in the cache", mCache.mCachedEntries.contains(key)); assertFalse("key found in the exclusives", mCache.mExclusiveEntries.contains(key)); } private void assertOrphanWithCount(CountingMemoryCache.Entry<String, Integer> entry, int count) { assertNotSame("entry found in the exclusives", entry, mCache.mCachedEntries.get(entry.key)); assertNotSame("entry found in the cache", entry, mCache.mExclusiveEntries.get(entry.key)); assertTrue("entry is not an orphan", entry.isOrphan); assertEquals("client count mismatch", count, entry.clientCount); } private void assertTotalSize(int count, int bytes) { assertEquals("total cache count mismatch", count, mCache.getCount()); assertEquals("total cache size mismatch", bytes, mCache.getSizeInBytes()); } private void assertExclusivelyOwnedSize(int count, int bytes) { assertEquals("total exclusives count mismatch", count, mCache.getEvictionQueueCount()); assertEquals("total exclusives size mismatch", bytes, mCache.getEvictionQueueSizeInBytes()); } }