1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.util.imagepool; 18 19 import org.junit.Test; 20 21 import android.util.imagepool.ImagePool.Image; 22 import android.util.imagepool.ImagePool.ImagePoolPolicy; 23 24 import java.awt.image.BufferedImage; 25 import java.lang.ref.SoftReference; 26 import java.util.concurrent.CountDownLatch; 27 import java.util.concurrent.TimeUnit; 28 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertNotEquals; 31 import static org.junit.Assert.assertNotNull; 32 import static org.junit.Assert.assertTrue; 33 34 public class ImagePoolImplTest { 35 36 private static final long TIMEOUT_SEC = 3; 37 38 @Test 39 public void testImagePoolInstance() { 40 ImagePool pool1 = ImagePoolProvider.get(); 41 ImagePool pool2 = ImagePoolProvider.get(); 42 assertNotNull(pool1); 43 assertNotNull(pool2); 44 assertEquals(pool1, pool2); 45 } 46 47 48 @Test 49 public void testImageDispose() throws InterruptedException { 50 int width = 700; 51 int height = 800; 52 int type = BufferedImage.TYPE_INT_ARGB; 53 CountDownLatch countDownLatch = new CountDownLatch(1); 54 ImagePoolImpl pool = getSimpleSingleBucketPool(width, height); 55 Image img1 = pool.acquire(width, height, type, 56 bufferedImage -> countDownLatch.countDown()); 57 BufferedImage img = getImg(img1); 58 assertNotNull(img); 59 img1 = null; 60 61 // ensure dispose actually loses buffered image link so it can be gc'd 62 gc(); 63 assertTrue(countDownLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS)); 64 } 65 @Test 66 public void testImageDisposeFromFunction() throws InterruptedException { 67 int width = 700; 68 int height = 800; 69 int type = BufferedImage.TYPE_INT_ARGB; 70 CountDownLatch cd = new CountDownLatch(1); 71 ImagePoolImpl pool = getSimpleSingleBucketPool(width, height); 72 73 BufferedImage img = createImageAndReturnBufferedImage(pool, width, height, type, cd); 74 assertNotNull(img); 75 76 // ensure dispose actually loses buffered image link so it can be gc'd 77 gc(); 78 assertTrue(cd.await(TIMEOUT_SEC, TimeUnit.SECONDS)); 79 } 80 81 @Test 82 public void testImageDisposedAndRecycled() throws InterruptedException { 83 int width = 700; 84 int height = 800; 85 int bucketWidth = 800; 86 int bucketHeight = 800; 87 int variant = 1; 88 int type = BufferedImage.TYPE_INT_ARGB; 89 ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( 90 new int[]{bucketWidth, bucketHeight}, 91 new int[]{1, 1}, 92 bucketHeight * bucketWidth * 4 * 3)); 93 94 // acquire first image and draw something. 95 BufferedImage bufferedImageForImg1; 96 final CountDownLatch countDownLatch1 = new CountDownLatch(1); 97 { 98 Image img1 = pool.acquire(width, height, type, 99 bufferedImage -> countDownLatch1.countDown()); 100 bufferedImageForImg1 = getImg(img1); 101 img1 = null; // this is still needed. 102 } 103 // dispose 104 gc(); 105 assertTrue(countDownLatch1.await(TIMEOUT_SEC, TimeUnit.SECONDS)); 106 107 // ensure dispose actually loses buffered image link so it can be gc'd 108 assertNotNull(bufferedImageForImg1); 109 assertEquals(bufferedImageForImg1.getWidth(), bucketWidth); 110 assertEquals(bufferedImageForImg1.getHeight(), bucketHeight); 111 112 // get 2nd image with the same spec 113 final CountDownLatch countDownLatch2 = new CountDownLatch(1); 114 BufferedImage bufferedImageForImg2; 115 { 116 Image img2 = pool.acquire(width - variant, height - variant, type, 117 bufferedImage -> countDownLatch2.countDown()); 118 bufferedImageForImg2 = getImg(img2); 119 assertEquals(bufferedImageForImg1, bufferedImageForImg2); 120 img2 = null; 121 } 122 // dispose 123 gc(); 124 assertTrue(countDownLatch2.await(TIMEOUT_SEC, TimeUnit.SECONDS)); 125 126 // ensure that we're recycling previously created buffered image. 127 assertNotNull(bufferedImageForImg1); 128 assertNotNull(bufferedImageForImg2); 129 } 130 131 132 @Test 133 public void testBufferedImageReleased() throws InterruptedException { 134 int width = 700; 135 int height = 800; 136 int bucketWidth = 800; 137 int bucketHeight = 800; 138 ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( 139 new int[]{bucketWidth, bucketHeight}, 140 new int[]{1, 1}, 141 bucketWidth * bucketWidth * 4 * 2)); 142 CountDownLatch countDownLatch = new CountDownLatch(1); 143 Image image1 = pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB, 144 bufferedImage -> countDownLatch.countDown()); 145 BufferedImage internalPtr = getImg(image1); 146 // dispose 147 image1 = null; 148 gc(); 149 assertTrue(countDownLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS)); 150 151 // Simulate BufferedBitmaps being gc'd. Bucket filled with null soft refs. 152 for (Bucket bucket : ((ImagePoolImpl) pool).mPool.values()) { 153 bucket.mBufferedImageRef.clear(); 154 bucket.mBufferedImageRef.add(new SoftReference<>(null)); 155 bucket.mBufferedImageRef.add(new SoftReference<>(null)); 156 } 157 158 assertNotEquals(internalPtr, 159 getImg(pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB))); 160 } 161 162 @Test 163 public void testPoolWidthHeightNotBigEnough() { 164 int width = 1000; 165 int height = 1000; 166 int bucketWidth = 999; 167 int bucketHeight = 800; 168 ImagePool pool = new ImagePoolImpl( 169 new ImagePoolPolicy(new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, 170 bucketWidth * bucketWidth * 4 * 2)); 171 ImageImpl image = (ImageImpl) pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB); 172 173 assertEquals(getTooBigForPoolCount(pool), 1); 174 } 175 176 @Test 177 public void testSizeNotBigEnough() { 178 int width = 500; 179 int height = 500; 180 int bucketWidth = 800; 181 int bucketHeight = 800; 182 ImagePoolImpl pool = new ImagePoolImpl( 183 new ImagePoolPolicy(new int[]{bucketWidth, bucketHeight}, new int[]{1, 1}, 184 bucketWidth * bucketWidth)); // cache not big enough. 185 ImageImpl image = (ImageImpl) pool.acquire(width, height, BufferedImage.TYPE_INT_ARGB); 186 187 assertEquals(getTooBigForPoolCount(pool), 1); 188 assertEquals(image.getWidth(), width); 189 assertEquals(image.getHeight(), height); 190 } 191 192 @Test 193 public void testImageMultipleCopies() throws InterruptedException { 194 int width = 700; 195 int height = 800; 196 int bucketWidth = 800; 197 int bucketHeight = 800; 198 int type = BufferedImage.TYPE_INT_ARGB; 199 ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( 200 new int[]{bucketWidth, bucketHeight}, 201 new int[]{2, 2}, 202 bucketHeight * bucketWidth * 4 * 4)); 203 204 // create 1, and 2 different instances. 205 final CountDownLatch cd1 = new CountDownLatch(1); 206 Image img1 = pool.acquire(width, height, type, bufferedImage -> cd1.countDown()); 207 BufferedImage bufferedImg1 = getImg(img1); 208 209 Image img2 = pool.acquire(width, height, type); 210 BufferedImage bufferedImg2 = getImg(img2); 211 212 assertNotEquals(bufferedImg1, bufferedImg2); 213 214 // disposing img1. Since # of copies == 2, this buffer should be recycled. 215 img1 = null; 216 gc(); 217 cd1.await(TIMEOUT_SEC, TimeUnit.SECONDS); 218 219 // Ensure bufferedImg1 is recycled in newly acquired img3. 220 Image img3 = pool.acquire(width, height, type); 221 BufferedImage bufferedImage3 = getImg(img3); 222 assertNotEquals(bufferedImg2, bufferedImage3); 223 assertEquals(bufferedImg1, bufferedImage3); 224 } 225 226 @Test 227 public void testPoolDispose() throws InterruptedException { 228 int width = 700; 229 int height = 800; 230 int bucketWidth = 800; 231 int bucketHeight = 800; 232 int type = BufferedImage.TYPE_INT_ARGB; 233 234 // Pool barely enough for 1 image. 235 ImagePoolImpl pool = new ImagePoolImpl(new ImagePoolPolicy( 236 new int[]{bucketWidth, bucketHeight}, 237 new int[]{2, 2}, 238 bucketHeight * bucketWidth * 4)); 239 240 // create 1, and 2 different instances. 241 final CountDownLatch cd1 = new CountDownLatch(1); 242 Image img1 = pool.acquire(width, height, type, bufferedImage -> cd1.countDown()); 243 BufferedImage bufferedImg1 = getImg(img1); 244 assertEquals(getAllocatedTotalBytes(pool), bucketWidth * bucketHeight * 4); 245 assertEquals(getTooBigForPoolCount(pool), 0); 246 247 // Release the img1. 248 img1 = null; 249 gc(); 250 cd1.await(TIMEOUT_SEC, TimeUnit.SECONDS); 251 252 // Dispose pool. 253 pool.dispose(); 254 assertEquals(getAllocatedTotalBytes(pool), 0); 255 256 // Request the same sized image as previous. 257 // If the pool was not disposed, this would return the image with bufferedImg1. 258 Image img2 = pool.acquire(width, height, type); 259 BufferedImage bufferedImg2 = getImg(img2); 260 assertEquals(getAllocatedTotalBytes(pool), bucketWidth * bucketHeight * 4); 261 assertEquals(getTooBigForPoolCount(pool), 0); 262 263 // Pool disposed before. No buffered image should be recycled. 264 assertNotEquals(img1, img2); 265 assertNotEquals(bufferedImg1, bufferedImg2); 266 } 267 268 private static BufferedImage createImageAndReturnBufferedImage(ImagePoolImpl pool, int width, 269 int height 270 , int type, CountDownLatch cd) { 271 Image img1 = pool.acquire(width, height, type, bufferedImage -> cd.countDown()); 272 return getImg(img1); 273 // At this point img1 should have no reference, causing finalizable to trigger 274 } 275 276 private static ImagePoolImpl getSimpleSingleBucketPool(int width, int height) { 277 278 int bucketWidth = Math.max(width, height); 279 int bucketHeight = Math.max(width, height); 280 return new ImagePoolImpl(new ImagePoolPolicy( 281 new int[]{bucketWidth, bucketHeight}, 282 new int[]{1, 1}, 283 bucketHeight * bucketWidth * 4 * 3)); 284 } 285 286 // Try to force a gc round 287 private static void gc() { 288 System.gc(); 289 System.gc(); 290 System.gc(); 291 } 292 293 private static int getTooBigForPoolCount(ImagePool pool) { 294 return ((ImagePoolStatsProdImpl) ((ImagePoolImpl) pool).mImagePoolStats).mTooBigForPoolCount; 295 } 296 297 private static long getAllocatedTotalBytes(ImagePool pool) { 298 return ((ImagePoolStatsProdImpl) ((ImagePoolImpl) pool).mImagePoolStats).mAllocateTotalBytes; 299 } 300 301 private static BufferedImage getImg(Image image) { 302 return ((ImageImpl) image).mImg; 303 } 304 } 305