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