/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.util.imagepool; import org.junit.Ignore; import org.junit.Test; import android.util.imagepool.ImagePool.Image; import android.util.imagepool.ImagePool.ImagePoolPolicy; import java.awt.image.BufferedImage; import java.lang.ref.SoftReference; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class ImagePoolImplTest { private static final long TIMEOUT_SEC = 3; @Test public void testImagePoolInstance() { ImagePool pool1 = ImagePoolProvider.get(); ImagePool pool2 = ImagePoolProvider.get(); assertNotNull(pool1); assertNotNull(pool2); assertEquals(pool1, pool2); } @Test public void testImageDispose() throws InterruptedException { int width = 700; int height = 800; int type = BufferedImage.TYPE_INT_ARGB; CountDownLatch countDownLatch = new CountDownLatch(1); ImagePoolImpl pool = getSimpleSingleBucketPool(width, height); Image img1 = pool.acquire(width, height, type, bufferedImage -> countDownLatch.countDown()); BufferedImage img = getImg(img1); assertNotNull(img); img1 = null; // ensure dispose actually loses buffered image link so it can be gc'd gc(); assertTrue(countDownLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS)); } @Test public void testImageDisposeFromFunction() throws InterruptedException { int width = 700; int height = 800; int type = BufferedImage.TYPE_INT_ARGB; CountDownLatch cd = new CountDownLatch(1); ImagePoolImpl pool = getSimpleSingleBucketPool(width, height); BufferedImage img = createImageAndReturnBufferedImage(pool, width, height, type, cd); assertNotNull(img); // ensure dispose actually loses buffered image link so it can be gc'd gc(); assertTrue(cd.await(TIMEOUT_SEC, TimeUnit.SECONDS)); } @Test public void testImageDisposedAndRecycled() throws InterruptedException { int width = 700; int height = 800; int bucketWidth = 800; int bucketHeight = 800; int variant = 1; int type = BufferedImage.TYPE_INT_ARGB; ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, bucketHeight * bucketWidth * 4 * 3)); // acquire first image and draw something. BufferedImage bufferedImageForImg1; final CountDownLatch countDownLatch1 = new CountDownLatch(1); { Image img1 = pool.acquire(width, height, type, bufferedImage -> countDownLatch1.countDown()); bufferedImageForImg1 = getImg(img1); img1 = null; // this is still needed. } // dispose gc(); assertTrue(countDownLatch1.await(TIMEOUT_SEC, TimeUnit.SECONDS)); // ensure dispose actually loses buffered image link so it can be gc'd assertNotNull(bufferedImageForImg1); assertEquals(bufferedImageForImg1.getWidth(), bucketWidth); assertEquals(bufferedImageForImg1.getHeight(), bucketHeight); // get 2nd image with the same spec final CountDownLatch countDownLatch2 = new CountDownLatch(1); BufferedImage bufferedImageForImg2; { Image img2 = pool.acquire(width - variant, height - variant, type, bufferedImage -> countDownLatch2.countDown()); bufferedImageForImg2 = getImg(img2); assertEquals(bufferedImageForImg1, bufferedImageForImg2); img2 = null; } // dispose gc(); assertTrue(countDownLatch2.await(TIMEOUT_SEC, TimeUnit.SECONDS)); // ensure that we're recycling previously created buffered image. assertNotNull(bufferedImageForImg1); assertNotNull(bufferedImageForImg2); } @Test public void testBufferedImageReleased() throws InterruptedException { int width = 700; int height = 800; int bucketWidth = 800; int bucketHeight = 800; ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, bucketWidth * bucketWidth * 4 * 2)); CountDownLatch countDownLatch = new CountDownLatch(1); Image image1 = pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB, bufferedImage -> countDownLatch.countDown()); BufferedImage internalPtr = getImg(image1); // dispose image1 = null; gc(); assertTrue(countDownLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS)); // Simulate BufferedBitmaps being gc'd. Bucket filled with null soft refs. for (Bucket bucket : ((ImagePoolImpl) pool).mPool.values()) { bucket.mBufferedImageRef.clear(); bucket.mBufferedImageRef.add(new SoftReference<>(null)); bucket.mBufferedImageRef.add(new SoftReference<>(null)); } assertNotEquals(internalPtr, getImg(pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB))); } @Test public void testPoolWidthHeightNotBigEnough() { int width = 1000; int height = 1000; int bucketWidth = 999; int bucketHeight = 800; ImagePool pool = new ImagePoolImpl( new ImagePoolPolicy(new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, bucketWidth * bucketWidth * 4 * 2)); ImageImpl image = (ImageImpl) pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB); assertEquals(getTooBigForPoolCount(pool), 1); } @Test public void testSizeNotBigEnough() { int width = 500; int height = 500; int bucketWidth = 800; int bucketHeight = 800; ImagePoolImpl pool = new ImagePoolImpl( new ImagePoolPolicy(new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, bucketWidth * bucketWidth)); // cache not big enough. ImageImpl image = (ImageImpl) pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB); assertEquals(getTooBigForPoolCount(pool), 1); assertEquals(image.getWidth(), width); assertEquals(image.getHeight(), height); } @Test public void testImageMultipleCopies() throws InterruptedException { int width = 700; int height = 800; int bucketWidth = 800; int bucketHeight = 800; int type = BufferedImage.TYPE_INT_ARGB; ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( new int[]{bucketWidth, bucketHeight}, new int[]{2, 2}, bucketHeight * bucketWidth * 4 * 4)); // create 1, and 2 different instances. final CountDownLatch cd1 = new CountDownLatch(1); Image img1 = pool.acquire(width, height, type, bufferedImage -> cd1.countDown()); BufferedImage bufferedImg1 = getImg(img1); Image img2 = pool.acquire(width, height, type); BufferedImage bufferedImg2 = getImg(img2); assertNotEquals(bufferedImg1, bufferedImg2); // disposing img1. Since # of copies == 2, this buffer should be recycled. img1 = null; gc(); cd1.await(TIMEOUT_SEC, TimeUnit.SECONDS); // Ensure bufferedImg1 is recycled in newly acquired img3. Image img3 = pool.acquire(width, height, type); BufferedImage bufferedImage3 = getImg(img3); assertNotEquals(bufferedImg2, bufferedImage3); assertEquals(bufferedImg1, bufferedImage3); } @Ignore("b/132614809") @Test public void testPoolDispose() throws InterruptedException { int width = 700; int height = 800; int bucketWidth = 800; int bucketHeight = 800; int type = BufferedImage.TYPE_INT_ARGB; // Pool barely enough for 1 image. ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( new int[]{bucketWidth, bucketHeight}, new int[]{2, 2}, bucketHeight * bucketWidth * 4)); // create 1, and 2 different instances. final CountDownLatch cd1 = new CountDownLatch(1); Image img1 = pool.acquire(width, height, type, bufferedImage -> cd1.countDown()); BufferedImage bufferedImg1 = getImg(img1); assertEquals(getAllocatedTotalBytes(pool), bucketWidth * bucketHeight * 4); assertEquals(getTooBigForPoolCount(pool), 0); // Release the img1. img1 = null; gc(); cd1.await(TIMEOUT_SEC, TimeUnit.SECONDS); // Dispose pool. pool.dispose(); assertEquals(getAllocatedTotalBytes(pool), 0); // Request the same sized image as previous. // If the pool was not disposed, this would return the image with bufferedImg1. Image img2 = pool.acquire(width, height, type); BufferedImage bufferedImg2 = getImg(img2); assertEquals(getAllocatedTotalBytes(pool), bucketWidth * bucketHeight * 4); assertEquals(getTooBigForPoolCount(pool), 0); // Pool disposed before. No buffered image should be recycled. assertNotEquals(img1, img2); assertNotEquals(bufferedImg1, bufferedImg2); } private static BufferedImage createImageAndReturnBufferedImage(ImagePoolImpl pool, int width, int height , int type, CountDownLatch cd) { Image img1 = pool.acquire(width, height, type, bufferedImage -> cd.countDown()); return getImg(img1); // At this point img1 should have no reference, causing finalizable to trigger } private static ImagePoolImpl getSimpleSingleBucketPool(int width, int height) { int bucketWidth = Math.max(width, height); int bucketHeight = Math.max(width, height); return new ImagePoolImpl(new ImagePoolPolicy( new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, bucketHeight * bucketWidth * 4 * 3)); } // Try to force a gc round private static void gc() { System.gc(); System.gc(); System.gc(); } private static int getTooBigForPoolCount(ImagePool pool) { return ((ImagePoolStatsProdImpl) ((ImagePoolImpl) pool).mImagePoolStats).mTooBigForPoolCount; } private static long getAllocatedTotalBytes(ImagePool pool) { return ((ImagePoolStatsProdImpl) ((ImagePoolImpl) pool).mImagePoolStats).mAllocateTotalBytes; } private static BufferedImage getImg(Image image) { return ((ImageImpl) image).mImg; } }