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 < 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 }