001    package com.thebuzzmedia.imgscalr;
002    
003    import java.awt.image.BufferedImage;
004    import java.awt.image.BufferedImageOp;
005    import java.util.concurrent.Callable;
006    import java.util.concurrent.ExecutorService;
007    import java.util.concurrent.Executors;
008    import java.util.concurrent.Future;
009    import java.util.concurrent.ThreadPoolExecutor;
010    import java.util.concurrent.TimeUnit;
011    
012    import com.thebuzzmedia.imgscalr.Scalr.Method;
013    import com.thebuzzmedia.imgscalr.Scalr.Mode;
014    import com.thebuzzmedia.imgscalr.Scalr.Rotation;
015    
016    /**
017     * Class used to provide the asynchronous versions of all the methods defined in
018     * {@link Scalr} for the purpose of offering more control over the scaling and
019     * ordering of a large number of scale operations.
020     * <p/>
021     * Given that image-scaling operations, especially when working with large
022     * images, can be very hardware-intensive (both CPU and memory), in large-scale
023     * deployments (e.g. a busy web application) it becomes increasingly important
024     * that the scale operations performed by imgscalr be manageable so as not to
025     * fire off too many simultaneous operations that the JVM's heap explodes and
026     * runs out of memory.
027     * <p/>
028     * Up until now it was left to the caller to implement their own serialization
029     * or limiting logic to handle these use-cases, but it was determined that this
030     * requirement be common enough that it should be integrated directly into the
031     * imgscalr library for everyone to benefit from.
032     * <p/>
033     * Every method in this class wraps the mirrored calls in the {@link Scalr}
034     * class in new {@link Callable} instances that are submitted to an internal
035     * {@link ExecutorService} for execution at a later date. A {@link Future} is
036     * returned to the caller representing the task that will perform the scale
037     * operation. {@link Future#get()} or {@link Future#get(long, TimeUnit)} can be
038     * used to block on the returned <code>Future</code>, waiting for the scale
039     * operation to complete and return the resultant {@link BufferedImage}.
040     * <p/>
041     * This design provides the following features:
042     * <ul>
043     * <li>Non-blocking, asynchronous scale operations that can continue execution
044     * while waiting on the scaled result.</li>
045     * <li>Serialize all scale requests down into a maximum number of
046     * <em>simultaneous</em> scale operations with no additional/complex logic. The
047     * number of simultaneous scale operations is caller-configurable so as best to
048     * optimize the host system (e.g. 1 scale thread per core).</li>
049     * <li>No need to worry about overloading the host system with too many scale
050     * operations, they will simply queue up in this class and execute in-order.</li>
051     * <li>Synchronous/blocking behavior can still be achieved by calling
052     * <code>get()</code> or <code>get(long, TimeUnit)</code> immediately on the
053     * returned {@link Future} from any of the methods below.</li>
054     * </ul>
055     * 
056     * This class also allows callers to provide their own (custom)
057     * {@link ExecutorService} for processing scale operations for maximum
058     * flexibility; otherwise this class utilizes a fixed {@link ThreadPoolExecutor}
059     * via {@link Executors#newFixedThreadPool(int)} that will create the given
060     * number of threads and let them sit idle, waiting for work.
061     * <h3>Performance</h3>
062     * When tuning this class for optimal performance, benchmarking your particular
063     * hardware is the best approach. For some rough guidelines though, there are
064     * two resources you want to watch closely:
065     * <ol>
066     * <li>JVM Heap Memory (Assume physical machine memory is always sufficiently
067     * large)</li>
068     * <li># of CPU Cores</li>
069     * </ol>
070     * You never want to allocate more scaling threads than you have CPU cores and
071     * on a sufficiently busy host where some of the cores may be busy running a
072     * database or a web server, you will want to allocate even less scaling
073     * threads.
074     * <p/>
075     * So as a maximum you would never want more scaling threads than CPU cores in
076     * any situation and less so on a busy server.
077     * <p/>
078     * If you allocate more threads than you have available CPU cores, your scaling
079     * operations will slow down as the CPU will spend a considerable amount of time
080     * context-switching between threads on the same core trying to finish all the
081     * tasks in parallel. You might still be tempted to do this because of the I/O
082     * delay some threads will encounter reading images off disk, but when you do
083     * your own benchmarking you'll likely find (as I did) that the actual disk I/O
084     * necessary to pull the image data off disk is a much smaller portion of the
085     * execution time than the actual scaling operations.
086     * <p/>
087     * If you are executing on a storage medium that is unexpectedly slow and I/O is
088     * a considerable portion of the scaling operation, feel free to try using more
089     * threads than CPU cores to see if that helps; but in most normal cases, it
090     * will only slow down all other parallel scaling operations.
091     * <p/>
092     * As for memory, every time an image is scaled it is decoded into a
093     * {@link BufferedImage} and stored in the JVM Heap space (decoded image
094     * instances are always larger than the source images on-disk). For larger
095     * images, that can use up quite a bit of memory. You will need to benchmark
096     * your particular use-cases on your hardware to get an idea of where the sweet
097     * spot is for this; if you are operating within tight memory bounds, you may
098     * want to limit simultaneous scaling operations to 1 or 2 regardless of the
099     * number of cores just to avoid having too many {@link BufferedImage} instances
100     * in JVM Heap space at the same time.
101     * <p/>
102     * These are rough metrics and behaviors to give you an idea of how best to tune
103     * this class for your deployment, but nothing can replacement writing a small
104     * Java class that scales a handful of images in a number of different ways and
105     * testing that directly on your deployment hardware. *
106     * <h3>Resource Overhead</h3>
107     * The {@link ExecutorService} utilized by this class won't be initialized until
108     * the class is referenced for the first time or explicitly set with one of the
109     * setter methods. More specifically, if you have no need for asynchronous image
110     * processing offered by this class, you don't need to worry about wasted
111     * resources or hanging/idle threads as they will never be created if you never
112     * reference this class.
113     * 
114     * @author Riyad Kalla (software@thebuzzmedia.com)
115     * @since 3.2
116     */
117    public class AsyncScalr {
118            /**
119             * Default thread count used to initialize the internal
120             * {@link ExecutorService} if a count isn't specified via
121             * {@link #setServiceThreadCount(int)} before this class is used.
122             * <p/>
123             * Default value is <code>2</code>.
124             */
125            public static final int DEFAULT_THREAD_COUNT = 2;
126    
127            private static ExecutorService service;
128    
129            /**
130             * Used to init the internal service with a 2-threaded, fixed thread pool if
131             * a custom one is not specified with either of the <code>init</code>
132             * methods.
133             */
134            static {
135                    setServiceThreadCount(DEFAULT_THREAD_COUNT);
136            }
137    
138            /**
139             * Used to get access to the internal {@link ExecutorService} used by this
140             * class to process scale operations.
141             * <p/>
142             * <strong>NOTE</strong>: You will need to explicitly shutdown any service
143             * currently set on this class before the host JVM exits <em>unless</em> you
144             * have passed in a custom {@link ExecutorService} that specifically
145             * creates/uses daemon threads (which will exit immediately).
146             * <p/>
147             * You can call {@link ExecutorService#shutdown()} to wait for all scaling
148             * operations to complete first or call
149             * {@link ExecutorService#shutdownNow()} to kill any in-process operations
150             * and purge all pending operations before exiting.
151             * 
152             * @return the current {@link ExecutorService} used by this class to process
153             *         scale operations.
154             */
155            public static ExecutorService getService() {
156                    return service;
157            }
158    
159            /**
160             * Used to initialize the internal {@link ExecutorService} which runs tasks
161             * generated by this class with the given service.
162             * <p/>
163             * <strong>NOTE</strong>: This operation will call
164             * {@link ExecutorService#shutdown()} on any existing
165             * {@link ExecutorService} currently set on this class. This means this
166             * operation will block until all pending (queued) scale operations are
167             * completed.
168             * 
169             * @param service
170             *            A specific {@link ExecutorService} instance that will be used
171             *            by this class to process scale operations.
172             * 
173             * @throws IllegalArgumentException
174             *             if <code>service</code> is <code>null</code>.
175             */
176            public static void setService(ExecutorService service)
177                            throws IllegalArgumentException {
178                    if (service == null)
179                            throw new IllegalArgumentException(
180                                            "service cannot be null; it must be a valid ExecutorService that can execute Callable tasks created by this class.");
181    
182                    /*
183                     * Shutdown any existing service, waiting for the last scale ops to
184                     * finish first.
185                     */
186                    if (AsyncScalr.service != null) {
187                            AsyncScalr.service.shutdown();
188                    }
189    
190                    AsyncScalr.service = service;
191            }
192    
193            /**
194             * Used to adjust the fixed number of threads (min/max) used by the internal
195             * {@link ThreadPoolExecutor} to executor scale operations.
196             * <p/>
197             * The following logic is used when applying thread count changes using this
198             * method:
199             * <ol>
200             * <li>If this is the first time the service is being initialized, a new
201             * {@link ThreadPoolExecutor} is created with the given fixed number of
202             * threads.</li>
203             * <li>If a service has already been set and it is of type
204             * {@link ThreadPoolExecutor} then the methods
205             * {@link ThreadPoolExecutor#setCorePoolSize(int)} and
206             * {@link ThreadPoolExecutor#setMaximumPoolSize(int)} are used to adjust the
207             * current fixed size of the thread pool without destroying the executor and
208             * creating a new one. This avoids unnecessary garbage for the GC and helps
209             * keep the task queue intact.</li>
210             * <li>If a service has already been set, but it is not of type
211             * {@link ThreadPoolExecutor}, then it will be shutdown after all pending
212             * tasks have completed and replaced with a new instance of type
213             * {@link ThreadPoolExecutor} with the given number of fixed threads.</li>
214             * </ol>
215             * 
216             * In the case where an existing {@link ThreadPoolExecutor} thread count is
217             * adjusted, if the given <code>threadCount</code> is smaller than the
218             * current number of threads in the pool, the extra threads will only be
219             * killed after they have completed their work and become idle. No scaling
220             * operations will be interrupted.
221             * 
222             * @param threadCount
223             *            The fixed number of threads (min/max) that the service will be
224             *            configured to use to process scale operations.
225             * 
226             * @throws IllegalArgumentException
227             *             if <code>threadCount</code> is &lt; 1.
228             */
229            public static void setServiceThreadCount(int threadCount)
230                            throws IllegalArgumentException {
231                    if (threadCount < 1)
232                            throw new IllegalArgumentException("threadCount [" + threadCount
233                                            + "] must be > 0.");
234    
235                    // Adjust the service if we can, otherwise replace it.
236                    if (AsyncScalr.service instanceof ThreadPoolExecutor) {
237                            ThreadPoolExecutor tpe = (ThreadPoolExecutor) AsyncScalr.service;
238    
239                            // Set the new min/max thread counts for the pool.
240                            tpe.setCorePoolSize(threadCount);
241                            tpe.setMaximumPoolSize(threadCount);
242                    } else
243                            setService(Executors.newFixedThreadPool(threadCount));
244            }
245    
246            public static Future<BufferedImage> resize(final BufferedImage src,
247                            final int targetSize, final BufferedImageOp... ops)
248                            throws IllegalArgumentException {
249                    return service.submit(new Callable<BufferedImage>() {
250                            public BufferedImage call() throws Exception {
251                                    return Scalr.resize(src, targetSize, ops);
252                            }
253                    });
254            }
255    
256            public static Future<BufferedImage> resize(final BufferedImage src,
257                            final Rotation rotation, final int targetSize,
258                            final BufferedImageOp... ops) throws IllegalArgumentException {
259                    return service.submit(new Callable<BufferedImage>() {
260                            public BufferedImage call() throws Exception {
261                                    return Scalr.resize(src, rotation, targetSize, ops);
262                            }
263                    });
264            }
265    
266            public static Future<BufferedImage> resize(final BufferedImage src,
267                            final Method scalingMethod, final int targetSize,
268                            final BufferedImageOp... ops) throws IllegalArgumentException {
269                    return service.submit(new Callable<BufferedImage>() {
270                            public BufferedImage call() throws Exception {
271                                    return Scalr.resize(src, scalingMethod, targetSize, ops);
272                            }
273                    });
274            }
275    
276            public static Future<BufferedImage> resize(final BufferedImage src,
277                            final Method scalingMethod, final Rotation rotation,
278                            final int targetSize, final BufferedImageOp... ops)
279                            throws IllegalArgumentException {
280                    return service.submit(new Callable<BufferedImage>() {
281                            public BufferedImage call() throws Exception {
282                                    return Scalr.resize(src, scalingMethod, rotation, targetSize,
283                                                    ops);
284                            }
285                    });
286            }
287    
288            public static Future<BufferedImage> resize(final BufferedImage src,
289                            final Mode resizeMode, final int targetSize,
290                            final BufferedImageOp... ops) throws IllegalArgumentException {
291                    return service.submit(new Callable<BufferedImage>() {
292                            public BufferedImage call() throws Exception {
293                                    return Scalr.resize(src, resizeMode, targetSize, ops);
294                            }
295                    });
296            }
297    
298            public static Future<BufferedImage> resize(final BufferedImage src,
299                            final Mode resizeMode, final Rotation rotation,
300                            final int targetSize, final BufferedImageOp... ops)
301                            throws IllegalArgumentException {
302                    return service.submit(new Callable<BufferedImage>() {
303                            public BufferedImage call() throws Exception {
304                                    return Scalr.resize(src, resizeMode, rotation, targetSize, ops);
305                            }
306                    });
307            }
308    
309            public static Future<BufferedImage> resize(final BufferedImage src,
310                            final Method scalingMethod, final Mode resizeMode,
311                            final int targetSize, final BufferedImageOp... ops)
312                            throws IllegalArgumentException {
313                    return service.submit(new Callable<BufferedImage>() {
314                            public BufferedImage call() throws Exception {
315                                    return Scalr.resize(src, scalingMethod, resizeMode, targetSize,
316                                                    ops);
317                            }
318                    });
319            }
320    
321            public static Future<BufferedImage> resize(final BufferedImage src,
322                            final Method scalingMethod, final Mode resizeMode,
323                            final Rotation rotation, final int targetSize,
324                            final BufferedImageOp... ops) throws IllegalArgumentException {
325                    return service.submit(new Callable<BufferedImage>() {
326                            public BufferedImage call() throws Exception {
327                                    return Scalr.resize(src, scalingMethod, resizeMode, rotation,
328                                                    targetSize, ops);
329                            }
330                    });
331            }
332    
333            public static Future<BufferedImage> resize(final BufferedImage src,
334                            final int targetWidth, final int targetHeight,
335                            final BufferedImageOp... ops) throws IllegalArgumentException {
336                    return service.submit(new Callable<BufferedImage>() {
337                            public BufferedImage call() throws Exception {
338                                    return Scalr.resize(src, targetWidth, targetHeight, ops);
339                            }
340                    });
341            }
342    
343            public static Future<BufferedImage> resize(final BufferedImage src,
344                            final Rotation rotation, final int targetWidth,
345                            final int targetHeight, final BufferedImageOp... ops)
346                            throws IllegalArgumentException {
347                    return service.submit(new Callable<BufferedImage>() {
348                            public BufferedImage call() throws Exception {
349                                    return Scalr.resize(src, rotation, targetWidth, targetHeight,
350                                                    ops);
351                            }
352                    });
353            }
354    
355            public static Future<BufferedImage> resize(final BufferedImage src,
356                            final Method scalingMethod, final int targetWidth,
357                            final int targetHeight, final BufferedImageOp... ops) {
358                    return service.submit(new Callable<BufferedImage>() {
359                            public BufferedImage call() throws Exception {
360                                    return Scalr.resize(src, scalingMethod, targetWidth,
361                                                    targetHeight, ops);
362                            }
363                    });
364            }
365    
366            public static Future<BufferedImage> resize(final BufferedImage src,
367                            final Method scalingMethod, final Rotation rotation,
368                            final int targetWidth, final int targetHeight,
369                            final BufferedImageOp... ops) {
370                    return service.submit(new Callable<BufferedImage>() {
371                            public BufferedImage call() throws Exception {
372                                    return Scalr.resize(src, scalingMethod, rotation, targetWidth,
373                                                    targetHeight, ops);
374                            }
375                    });
376            }
377    
378            public static Future<BufferedImage> resize(final BufferedImage src,
379                            final Mode resizeMode, final int targetWidth,
380                            final int targetHeight, final BufferedImageOp... ops)
381                            throws IllegalArgumentException {
382                    return service.submit(new Callable<BufferedImage>() {
383                            public BufferedImage call() throws Exception {
384                                    return Scalr.resize(src, resizeMode, targetWidth, targetHeight,
385                                                    ops);
386                            }
387                    });
388            }
389    
390            public static Future<BufferedImage> resize(final BufferedImage src,
391                            final Mode resizeMode, final Rotation rotation,
392                            final int targetWidth, final int targetHeight,
393                            final BufferedImageOp... ops) throws IllegalArgumentException {
394                    return service.submit(new Callable<BufferedImage>() {
395                            public BufferedImage call() throws Exception {
396                                    return Scalr.resize(src, resizeMode, rotation, targetWidth,
397                                                    targetHeight, ops);
398                            }
399                    });
400            }
401    
402            public static Future<BufferedImage> resize(final BufferedImage src,
403                            final Method scalingMethod, final Mode resizeMode,
404                            final int targetWidth, final int targetHeight,
405                            final BufferedImageOp... ops) throws IllegalArgumentException {
406                    return service.submit(new Callable<BufferedImage>() {
407                            public BufferedImage call() throws Exception {
408                                    return Scalr.resize(src, scalingMethod, resizeMode,
409                                                    targetWidth, targetHeight, ops);
410                            }
411                    });
412            }
413    
414            public static Future<BufferedImage> resize(final BufferedImage src,
415                            final Method scalingMethod, final Mode resizeMode,
416                            final Rotation rotation, final int targetWidth,
417                            final int targetHeight, final BufferedImageOp... ops)
418                            throws IllegalArgumentException {
419                    return service.submit(new Callable<BufferedImage>() {
420                            public BufferedImage call() throws Exception {
421                                    return Scalr.resize(src, scalingMethod, resizeMode, rotation,
422                                                    targetWidth, targetHeight, ops);
423                            }
424                    });
425            }
426    }