/* * 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.memory; import android.util.SparseIntArray; import com.facebook.common.memory.MemoryTrimmableRegistry; import com.facebook.imagepipeline.memory.BasePool.PoolSizeViolationException; import com.facebook.testing.robolectric.v2.WithTestDefaultsRunner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static org.mockito.Mockito.mock; /** * Tests for BasePool */ @RunWith(WithTestDefaultsRunner.class) public class BasePoolTest { private TestPool mPool; private PoolStats<byte[]> mStats; @Before public void setup() { mPool = new TestPool(10, 14); mStats = new PoolStats<byte[]>(mPool); } // Test out the alloc method @Test public void testAlloc() throws Exception { Assert.assertEquals(1, mPool.alloc(1).length); Assert.assertEquals(3, mPool.alloc(3).length); Assert.assertEquals(2, mPool.alloc(2).length); } @Test public void testFree() throws Exception { } // tests out the getBucketedSize method @Test public void testGetBucketedSize() throws Exception { Assert.assertEquals(2, mPool.getBucketedSize(1)); Assert.assertEquals(2, mPool.getBucketedSize(2)); Assert.assertEquals(4, mPool.getBucketedSize(3)); Assert.assertEquals(6, mPool.getBucketedSize(6)); Assert.assertEquals(8, mPool.getBucketedSize(7)); } // tests out the getBucketedSize method for invalid inputs @Test public void testGetBucketedSize_Invalid() throws Exception { int[] sizes = new int[] {-1, 0}; for (int s: sizes) { try { mPool.getBucketedSize(s); Assert.fail("Failed size: " + s); } catch (BasePool.InvalidSizeException e) { // do nothing } } } // tests out the getBucketedSizeForValue method @Test public void testGetBucketedSizeForValue() throws Exception { Assert.assertEquals(2, mPool.getBucketedSizeForValue(new byte[2])); Assert.assertEquals(3, mPool.getBucketedSizeForValue(new byte[3])); Assert.assertEquals(6, mPool.getBucketedSizeForValue(new byte[6])); } @Test public void testGetSizeInBytes() throws Exception { Assert.assertEquals(1, mPool.getSizeInBytes(1)); Assert.assertEquals(2, mPool.getSizeInBytes(2)); Assert.assertEquals(3, mPool.getSizeInBytes(3)); Assert.assertEquals(5, mPool.getSizeInBytes(5)); Assert.assertEquals(4, mPool.getSizeInBytes(4)); } // Get via alloc @Test public void testGet_Alloc() throws Exception { // get a buffer - causes an alloc byte[] b1 = mPool.get(1); Assert.assertNotNull(b1); Assert.assertEquals(2, b1.length); Assert.assertTrue(mPool.mInUseValues.contains(b1)); mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); // release this buffer mPool.release(b1); Assert.assertFalse(mPool.mInUseValues.contains(b1)); // get another buffer, but of a different size. No reuse possible byte[] b2 = mPool.get(3); Assert.assertNotNull(b2); Assert.assertEquals(4, b2.length); Assert.assertTrue(mPool.mInUseValues.contains(b2)); mStats.refresh(); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(0, 1), 4, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(2, mStats.mFreeBytes); Assert.assertEquals(4, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); } // Get via alloc+trim @Test public void testGet_AllocAndTrim() throws Exception { mPool = new TestPool(10, 10, makeBucketSizeArray(2, 2, 4, 2, 6, 2)); mStats.setPool(mPool); // allocate and release multiple buffers byte[] b1; b1 = mPool.get(2); mPool.release(b1); b1 = mPool.get(6); mPool.release(b1); // get current stats mStats.refresh(); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(0, 1), 4, new IntPair(0, 0), 6, new IntPair(0, 1)), mStats.mBucketStats); // get a new buffer; this should cause an alloc and a trim mPool.get(3); mStats.refresh(); // validate stats Assert.assertEquals( ImmutableMap.of( 2, new IntPair(0, 0), 4, new IntPair(1, 0), 6, new IntPair(0, 1)), mStats.mBucketStats); Assert.assertEquals(6, mStats.mFreeBytes); Assert.assertEquals(4, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); } // Tests that we can reuse a free buffer in the pool @Test public void testGet_Reuse() throws Exception { // get a buffer, and immediately release it byte[] b1 = mPool.get(1); mPool.release(b1); Assert.assertNotNull(b1); Assert.assertEquals(2, b1.length); mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(0, 1)), mStats.mBucketStats); Assert.assertEquals(2, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mFreeCount); Assert.assertEquals(0, mStats.mUsedCount); // get another buffer of the same size as above. We should be able to reuse it byte[] b2 = mPool.get(1); Assert.assertNotNull(b2); Assert.assertEquals(2, b2.length); Assert.assertTrue(mPool.mInUseValues.contains(b2)); mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); } // Get via alloc - exception on max size hard cap @Test public void testGet_AllocFailure() throws Exception { TestPool pool = new TestPool(4, 5); pool.get(4); try { pool.get(4); Assert.fail(); } catch (PoolSizeViolationException e) { // expected exception } } // test a simple release @Test public void testRelease() throws Exception { // get a buffer - causes an alloc byte[] b1 = mPool.get(1); // release this buffer mPool.release(b1); // verify stats mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(0, 1)), mStats.mBucketStats); Assert.assertEquals(2, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mFreeCount); Assert.assertEquals(0, mStats.mUsedCount); } // test out release(), when it should free the value, instead of adding to the pool @Test public void testRelease_Free() throws Exception { // get a set of buffers that bump up above the max size mPool.get(6); // get and release another buffer. this should cause a free byte[] b3 = mPool.get(6); mPool.release(b3); mStats.refresh(); Assert.assertEquals( ImmutableMap.of(6, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(6, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); } // test release on zero-sized pool @Test public void testRelease_Free2() throws Exception { // create a new pool with a max size cap of zero. mPool = new TestPool(0, 10); mStats.setPool(mPool); // get a buffer and release it - this should trigger the soft cap byte[] b1 = mPool.get(4); mPool.release(b1); mStats.refresh(); Assert.assertEquals( ImmutableMap.of( 4, new IntPair(0, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(0, mStats.mUsedCount); } // Test release with bucket length constraints @Test public void testRelease_BucketLengths() throws Exception { mPool = new TestPool(Integer.MAX_VALUE, Integer.MAX_VALUE, makeBucketSizeArray(2, 2)); mStats.setPool(mPool); byte[] b0 = mPool.get(2); mPool.get(2); mPool.get(2); mStats.refresh(); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(3, 0)), mStats.mBucketStats); Assert.assertEquals(6, mStats.mUsedBytes); Assert.assertEquals(3, mStats.mUsedCount); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mFreeCount); // now release one of the buffers mPool.release(b0); mStats.refresh(); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(2, 0)), mStats.mBucketStats); Assert.assertEquals(4, mStats.mUsedBytes); Assert.assertEquals(2, mStats.mUsedCount); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mFreeCount); } // Test releasing an 'unknown' value @Test public void testRelease_UnknownValue() throws Exception { // get a buffer from the pool mPool.get(1); // allocate a buffer outside the pool byte[] b2 = new byte[2]; // try to release this buffer to the pool mPool.release(b2); // verify stats mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); } // Test 'taking-over' a value @Test public void testTakeOver() throws Exception { // allocate a buffer outside the pool byte[] b2 = new byte[2]; Assert.assertTrue(mPool.takeOver(b2)); // verify stats mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(1, mStats.mUsedCount); // try to take it over again. Nothing should change Assert.assertTrue(mPool.takeOver(b2)); mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); byte[] b3 = new byte[14]; Assert.assertFalse(mPool.takeOver(b3)); Assert.assertEquals( ImmutableMap.of(2, new IntPair(1, 0)), mStats.mBucketStats); } // test out release with non reusable values @Test public void testRelease_NonReusable() throws Exception { TestPool pool = new TestPool(100, 100, makeBucketSizeArray(2, 3)); mPool.mIsReusable = false; mStats.setPool(pool); // get a buffer, and then release it byte[] b1 = mPool.get(2); mPool.release(b1); // verify stats mStats.refresh(); Assert.assertEquals( ImmutableMap.of(2, new IntPair(0, 0)), mStats.mBucketStats); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mUsedBytes); Assert.assertEquals(0, mStats.mFreeCount); Assert.assertEquals(0, mStats.mUsedCount); } // test buffers outside the 'normal' bucket sizes @Test public void testGetRelease_NonBucketSizes() throws Exception { mPool = new TestPool(10, 10, makeBucketSizeArray(2, 1, 4, 1, 6, 1)); mStats.setPool(mPool); mPool.get(2); byte[] b1 = mPool.get(7); mStats.refresh(); Assert.assertEquals(10, mStats.mUsedBytes); Assert.assertEquals(2, mStats.mUsedCount); mPool.release(b1); mStats.refresh(); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mUsedCount); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mFreeCount); byte[] b2 = new byte[3]; mPool.release(b2); mStats.refresh(); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals(1, mStats.mUsedCount); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(0, mStats.mFreeCount); } // test illegal arguments to get @Test public void testGetWithErrors() throws Exception { int[] sizes = new int[] {-1, 0}; for (int s: sizes) { try { mPool.get(s); Assert.fail("Failed size: " + s); } catch (BasePool.InvalidSizeException e) { // do nothing } } } // test out trimToNothing functionality @Test public void testTrimToNothing() throws Exception { // alloc a buffer and then release it byte[] b1 = mPool.get(1); mPool.release(b1); mPool.get(3); mStats.refresh(); Assert.assertEquals(2, mStats.mFreeBytes); Assert.assertEquals(4, mStats.mUsedBytes); // trim the pool and check mPool.trimToNothing(); mStats.refresh(); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(4, mStats.mUsedBytes); } // test out trimToSize functionality @Test public void testTrimToSize() throws Exception { mPool = new TestPool(100, 100, makeBucketSizeArray(2, 2, 4, 2, 6, 2)); mStats.setPool(mPool); // allocate and release multiple buffers byte[] b1; mPool.get(2); b1 = mPool.get(2); mPool.release(b1); b1 = mPool.get(6); mPool.release(b1); b1 = mPool.get(4); mPool.release(b1); mStats.refresh(); Assert.assertEquals(12, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); // perform a dummy trim - nothing should happen mPool.trimToSize(100); mStats.refresh(); Assert.assertEquals(12, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); // now perform the real trim mPool.trimToSize(8); mStats.refresh(); Assert.assertEquals(6, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(1, 0), 4, new IntPair(0, 0), 6, new IntPair(0, 1)), mStats.mBucketStats); // perform another trim mPool.trimToSize(1); mStats.refresh(); Assert.assertEquals(0, mStats.mFreeBytes); Assert.assertEquals(2, mStats.mUsedBytes); Assert.assertEquals( ImmutableMap.of( 2, new IntPair(1, 0), 4, new IntPair(0, 0), 6, new IntPair(0, 0)), mStats.mBucketStats); } @Test public void test_canAllocate() throws Exception { TestPool pool = new TestPool(4, 8); pool.get(4); Assert.assertFalse(pool.isMaxSizeSoftCapExceeded()); Assert.assertTrue(pool.canAllocate(2)); pool.get(2); Assert.assertTrue(pool.isMaxSizeSoftCapExceeded()); Assert.assertTrue(pool.canAllocate(2)); Assert.assertFalse(pool.canAllocate(4)); } /** * A simple test pool that allocates byte arrays, and always allocates buffers of double * the size requested */ public static class TestPool extends BasePool<byte[]> { public boolean mIsReusable; public TestPool(int maxPoolSizeSoftCap, int maxPoolSizeHardCap) { this(maxPoolSizeSoftCap, maxPoolSizeHardCap, null); } public TestPool( int maxPoolSizeSoftCap, int maxPoolSizeHardCap, SparseIntArray bucketSizes) { super( mock(MemoryTrimmableRegistry.class), new PoolParams(maxPoolSizeSoftCap, maxPoolSizeHardCap, bucketSizes), mock(PoolStatsTracker.class)); mIsReusable = true; initialize(); } @Override protected byte[] alloc(int bucketedSize) { return new byte[bucketedSize]; } @Override protected void free(byte[] value) { } @Override protected boolean isReusable(byte[] value) { return mIsReusable; } /** * Allocate the smallest even number than is greater than or equal to the requested size * @param requestSize the logical request size * @return the slightly higher size */ @Override protected int getBucketedSize(int requestSize) { if (requestSize <= 0) { throw new InvalidSizeException(requestSize); } return (requestSize % 2 == 0) ? requestSize : requestSize + 1; } @Override protected int getBucketedSizeForValue(byte[] value) { return value.length; } @Override protected int getSizeInBytes(int bucketedSize) { return bucketedSize; } } private static SparseIntArray makeBucketSizeArray(int... params) { Preconditions.checkArgument(params.length % 2 == 0); final SparseIntArray bucketSizes = new SparseIntArray(); for (int i = 0; i < params.length; i += 2) { bucketSizes.append(params[i], params[i + 1]); } return bucketSizes; } }