package com.thebuzzmedia.imgscalr; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import com.thebuzzmedia.imgscalr.Scalr.Method; import com.thebuzzmedia.imgscalr.Scalr.Mode; import com.thebuzzmedia.imgscalr.Scalr.Rotation; /** * Class used to provide the asynchronous versions of all the methods defined in * {@link Scalr} for the purpose of offering more control over the scaling and * ordering of a large number of scale operations. * <p/> * Given that image-scaling operations, especially when working with large * images, can be very hardware-intensive (both CPU and memory), in large-scale * deployments (e.g. a busy web application) it becomes increasingly important * that the scale operations performed by imgscalr be manageable so as not to * fire off too many simultaneous operations that the JVM's heap explodes and * runs out of memory. * <p/> * Up until now it was left to the caller to implement their own serialization * or limiting logic to handle these use-cases, but it was determined that this * requirement be common enough that it should be integrated directly into the * imgscalr library for everyone to benefit from. * <p/> * Every method in this class wraps the mirrored calls in the {@link Scalr} * class in new {@link Callable} instances that are submitted to an internal * {@link ExecutorService} for execution at a later date. A {@link Future} is * returned to the caller representing the task that will perform the scale * operation. {@link Future#get()} or {@link Future#get(long, TimeUnit)} can be * used to block on the returned <code>Future</code>, waiting for the scale * operation to complete and return the resultant {@link BufferedImage}. * <p/> * This design provides the following features: * <ul> * <li>Non-blocking, asynchronous scale operations that can continue execution * while waiting on the scaled result.</li> * <li>Serialize all scale requests down into a maximum number of * <em>simultaneous</em> scale operations with no additional/complex logic. The * number of simultaneous scale operations is caller-configurable so as best to * optimize the host system (e.g. 1 scale thread per core).</li> * <li>No need to worry about overloading the host system with too many scale * operations, they will simply queue up in this class and execute in-order.</li> * <li>Synchronous/blocking behavior can still be achieved by calling * <code>get()</code> or <code>get(long, TimeUnit)</code> immediately on the * returned {@link Future} from any of the methods below.</li> * </ul> * * This class also allows callers to provide their own (custom) * {@link ExecutorService} for processing scale operations for maximum * flexibility; otherwise this class utilizes a fixed {@link ThreadPoolExecutor} * via {@link Executors#newFixedThreadPool(int)} that will create the given * number of threads and let them sit idle, waiting for work. * <h3>Performance</h3> * When tuning this class for optimal performance, benchmarking your particular * hardware is the best approach. For some rough guidelines though, there are * two resources you want to watch closely: * <ol> * <li>JVM Heap Memory (Assume physical machine memory is always sufficiently * large)</li> * <li># of CPU Cores</li> * </ol> * You never want to allocate more scaling threads than you have CPU cores and * on a sufficiently busy host where some of the cores may be busy running a * database or a web server, you will want to allocate even less scaling * threads. * <p/> * So as a maximum you would never want more scaling threads than CPU cores in * any situation and less so on a busy server. * <p/> * If you allocate more threads than you have available CPU cores, your scaling * operations will slow down as the CPU will spend a considerable amount of time * context-switching between threads on the same core trying to finish all the * tasks in parallel. You might still be tempted to do this because of the I/O * delay some threads will encounter reading images off disk, but when you do * your own benchmarking you'll likely find (as I did) that the actual disk I/O * necessary to pull the image data off disk is a much smaller portion of the * execution time than the actual scaling operations. * <p/> * If you are executing on a storage medium that is unexpectedly slow and I/O is * a considerable portion of the scaling operation, feel free to try using more * threads than CPU cores to see if that helps; but in most normal cases, it * will only slow down all other parallel scaling operations. * <p/> * As for memory, every time an image is scaled it is decoded into a * {@link BufferedImage} and stored in the JVM Heap space (decoded image * instances are always larger than the source images on-disk). For larger * images, that can use up quite a bit of memory. You will need to benchmark * your particular use-cases on your hardware to get an idea of where the sweet * spot is for this; if you are operating within tight memory bounds, you may * want to limit simultaneous scaling operations to 1 or 2 regardless of the * number of cores just to avoid having too many {@link BufferedImage} instances * in JVM Heap space at the same time. * <p/> * These are rough metrics and behaviors to give you an idea of how best to tune * this class for your deployment, but nothing can replacement writing a small * Java class that scales a handful of images in a number of different ways and * testing that directly on your deployment hardware. * * <h3>Resource Overhead</h3> * The {@link ExecutorService} utilized by this class won't be initialized until * the class is referenced for the first time or explicitly set with one of the * setter methods. More specifically, if you have no need for asynchronous image * processing offered by this class, you don't need to worry about wasted * resources or hanging/idle threads as they will never be created if you never * reference this class. * * @author Riyad Kalla (software@thebuzzmedia.com) * @since 3.2 */ public class AsyncScalr { /** * Default thread count used to initialize the internal * {@link ExecutorService} if a count isn't specified via * {@link #setServiceThreadCount(int)} before this class is used. * <p/> * Default value is <code>2</code>. */ public static final int DEFAULT_THREAD_COUNT = 2; private static ExecutorService service; /** * Used to init the internal service with a 2-threaded, fixed thread pool if * a custom one is not specified with either of the <code>init</code> * methods. */ static { setServiceThreadCount(DEFAULT_THREAD_COUNT); } /** * Used to get access to the internal {@link ExecutorService} used by this * class to process scale operations. * <p/> * <strong>NOTE</strong>: You will need to explicitly shutdown any service * currently set on this class before the host JVM exits <em>unless</em> you * have passed in a custom {@link ExecutorService} that specifically * creates/uses daemon threads (which will exit immediately). * <p/> * You can call {@link ExecutorService#shutdown()} to wait for all scaling * operations to complete first or call * {@link ExecutorService#shutdownNow()} to kill any in-process operations * and purge all pending operations before exiting. * * @return the current {@link ExecutorService} used by this class to process * scale operations. */ public static ExecutorService getService() { return service; } /** * Used to initialize the internal {@link ExecutorService} which runs tasks * generated by this class with the given service. * <p/> * <strong>NOTE</strong>: This operation will call * {@link ExecutorService#shutdown()} on any existing * {@link ExecutorService} currently set on this class. This means this * operation will block until all pending (queued) scale operations are * completed. * * @param service * A specific {@link ExecutorService} instance that will be used * by this class to process scale operations. * * @throws IllegalArgumentException * if <code>service</code> is <code>null</code>. */ public static void setService(ExecutorService service) throws IllegalArgumentException { if (service == null) throw new IllegalArgumentException( "service cannot be null; it must be a valid ExecutorService that can execute Callable tasks created by this class."); /* * Shutdown any existing service, waiting for the last scale ops to * finish first. */ if (AsyncScalr.service != null) { AsyncScalr.service.shutdown(); } AsyncScalr.service = service; } /** * Used to adjust the fixed number of threads (min/max) used by the internal * {@link ThreadPoolExecutor} to executor scale operations. * <p/> * The following logic is used when applying thread count changes using this * method: * <ol> * <li>If this is the first time the service is being initialized, a new * {@link ThreadPoolExecutor} is created with the given fixed number of * threads.</li> * <li>If a service has already been set and it is of type * {@link ThreadPoolExecutor} then the methods * {@link ThreadPoolExecutor#setCorePoolSize(int)} and * {@link ThreadPoolExecutor#setMaximumPoolSize(int)} are used to adjust the * current fixed size of the thread pool without destroying the executor and * creating a new one. This avoids unnecessary garbage for the GC and helps * keep the task queue intact.</li> * <li>If a service has already been set, but it is not of type * {@link ThreadPoolExecutor}, then it will be shutdown after all pending * tasks have completed and replaced with a new instance of type * {@link ThreadPoolExecutor} with the given number of fixed threads.</li> * </ol> * * In the case where an existing {@link ThreadPoolExecutor} thread count is * adjusted, if the given <code>threadCount</code> is smaller than the * current number of threads in the pool, the extra threads will only be * killed after they have completed their work and become idle. No scaling * operations will be interrupted. * * @param threadCount * The fixed number of threads (min/max) that the service will be * configured to use to process scale operations. * * @throws IllegalArgumentException * if <code>threadCount</code> is < 1. */ public static void setServiceThreadCount(int threadCount) throws IllegalArgumentException { if (threadCount < 1) throw new IllegalArgumentException("threadCount [" + threadCount + "] must be > 0."); // Adjust the service if we can, otherwise replace it. if (AsyncScalr.service instanceof ThreadPoolExecutor) { ThreadPoolExecutor tpe = (ThreadPoolExecutor) AsyncScalr.service; // Set the new min/max thread counts for the pool. tpe.setCorePoolSize(threadCount); tpe.setMaximumPoolSize(threadCount); } else setService(Executors.newFixedThreadPool(threadCount)); } public static Future<BufferedImage> resize(final BufferedImage src, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Rotation rotation, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, rotation, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Rotation rotation, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, rotation, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Mode resizeMode, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, resizeMode, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Mode resizeMode, final Rotation rotation, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, resizeMode, rotation, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Mode resizeMode, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, resizeMode, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Mode resizeMode, final Rotation rotation, final int targetSize, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, resizeMode, rotation, targetSize, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Rotation rotation, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, rotation, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Rotation rotation, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, rotation, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Mode resizeMode, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, resizeMode, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Mode resizeMode, final Rotation rotation, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, resizeMode, rotation, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Mode resizeMode, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, resizeMode, targetWidth, targetHeight, ops); } }); } public static Future<BufferedImage> resize(final BufferedImage src, final Method scalingMethod, final Mode resizeMode, final Rotation rotation, final int targetWidth, final int targetHeight, final BufferedImageOp... ops) throws IllegalArgumentException { return service.submit(new Callable<BufferedImage>() { public BufferedImage call() throws Exception { return Scalr.resize(src, scalingMethod, resizeMode, rotation, targetWidth, targetHeight, ops); } }); } }