/*
* @(#)ResourcePool.java
*
* $Date: 2014-06-06 20:04:49 +0200 (P, 06 jún. 2014) $
*
* Copyright (c) 2014 by Jeremy Wood.
* All rights reserved.
*
* The copyright of this software is owned by Jeremy Wood.
* You may not use, copy or modify this software, except in
* accordance with the license agreement you entered into with
* Jeremy Wood. For details see accompanying license terms.
*
* This software is probably, but not necessarily, discussed here:
* https://javagraphics.java.net/
*
* That site should also contain the most recent official version
* of this software. (See the SVN repository for more details.)
*/
package com.bric.util;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/** This object manages a pool of resources (arrays and images)
* for frequent repetitive reuse.
* <p>Sometimes constantly reallocating
* resources in tight loops can be an expensive drain, so this
* should help relieve that performance expense.
* <p>This pool also has limitations, though, so it does not
* introduce a memory leak. Strong references to resources are
* purged after a fixed time interval (usually 5 seconds). Also
* there are limits to the number of resources of any given
* type that will be stored. There are two such limits:
* <ul><li>A limit of the exact resource type. For example: by default
* there will only be 5 images of the same width, height and type.</li>
* <li>A limit of all resources of a given type. For example: by
* default there will only be 20 images total.</li></ul>
* <p>(Arrays have a higher tolerance.)
* <p>It is still possible to abuse this model and introduce
* memory problems, in the same way that is possible to run
* out of memory just by constructing a few 10,000x10,000 images.
* But when used responsibly, this should be a valuable tool to
* easy the cost of constructing thousands of similar objects.
* <p>Every time you retrieve an object from this pool, you should
* wrap the following code in a try/finally block and return
* the object back to this pool when finished.
* @see ResourcePoolDemo
*/
public class ResourcePool {
private enum Type { IMAGE, FLOAT, INT, DOUBLE, SHORT, LONG, BYTE };
/** The limit for each type of resource this pool manages. */
public static class Limit {
int totalLimit, groupLimit;
double timeLimit;
/**
*
* @param groupLimit the number of identical instances this pool stores.
* @param totalLimit the total number of objects of a given type this stores.
* @param timeLimit the number of seconds this stores a resource.
*/
public Limit(int groupLimit,int totalLimit,double timeLimit) {
this.totalLimit = totalLimit;
this.groupLimit = groupLimit;
this.timeLimit = timeLimit;
}
}
private static Set<WeakReference<ResourcePool>> allPools = new HashSet<WeakReference<ResourcePool>>();
static {
new Thread("ResourcePool Cleanup") {
public void run() {
while(true) {
synchronized(allPools) {
Iterator<WeakReference<ResourcePool>> iter = allPools.iterator();
while(iter.hasNext()) {
WeakReference<ResourcePool> ref = iter.next();
ResourcePool pool = ref.get();
if(pool==null) {
iter.remove();
} else {
pool.clean();
}
}
}
try {
Thread.sleep(5000);
} catch(Exception e) {}
}
}
}.start();
}
private static long recordIDCtr = 0;
private static class Record {
Object resource;
long timestamp = System.currentTimeMillis();
long id = recordIDCtr++;
public Record(Object resource) {
this.resource = resource;
}
}
private static Comparator<Record> recordComparator = new Comparator<Record>() {
public int compare(Record o1, Record o2) {
if(o1.timestamp<o2.timestamp)
return -1;
if(o1.timestamp>o2.timestamp)
return 1;
if(o1.id<o2.id)
return -1;
if(o1.id>o2.id)
return 1;
return 0;
}
};
private static ResourcePool globalPool = new ResourcePool();
/** Return the default <code>ResourcePool</code>.
*/
public static ResourcePool get() {
return globalPool;
}
Map<Type, Limit> limits = new HashMap<Type, Limit>();
Map<Type, Map<Long, SortedSet<Record>>> resources = new HashMap<Type, Map<Long, SortedSet<Record>>>();
/** Create a new <code>ResourcePool</code>.
*/
public ResourcePool() {
synchronized(allPools) {
allPools.add(new WeakReference<ResourcePool>(this));
}
limits.put( Type.IMAGE, new Limit(5, 20, 5.0) );
}
/** Iterate over all resources and make sure nothing has expired
* based on the current time limits and time stamps.
*
*/
protected synchronized void clean() {
long currentTime = System.currentTimeMillis();
for(Type type : resources.keySet()) {
Limit limit = getLimit(type);
long timelimit = (long)(limit.timeLimit*1000);
Map<Long, SortedSet<Record>> group = resources.get(type);
for(Long id : group.keySet()) {
SortedSet<Record> r = group.get(id);
Iterator<Record> i = r.iterator();
while(i.hasNext()) {
Record record = i.next();
if(record.timestamp+timelimit<currentTime) {
//System.out.println("cleaning "+type+", "+id+", "+record.resource);
i.remove();
}
}
}
}
}
private static final Limit genericLimit = new Limit(1000, 2000, 5.0);
/** Return the Limit for a given resource type. */
protected Limit getLimit(Type type) {
Limit l = limits.get(type);
if(l==null) return genericLimit;
return l;
}
/** Return a BufferedImage from this pool. If no cached
* images exists matching these parameters: then a new image
* is created.
* @param width the width of the image.
* @param height the height of the image.
* @param type the type of the image.
* @param clear if true and a cached image is identified: then
* that image is first cleared (using an AlphaComposite.Clear).
* @return an image matching the arguments provided. If possible this
* will recycle a previously cached image.
*/
public synchronized BufferedImage getImage(int width,int height,int type,boolean clear) {
long id = (width << 16) + (height << 8) + type;
BufferedImage bi = (BufferedImage)get( Type.IMAGE, id);
if(bi!=null) {
if(clear) {
Graphics2D g = bi.createGraphics();
g.setComposite(AlphaComposite.Clear);
g.fillRect(0,0,width,height);
g.dispose();
}
return bi;
}
return new BufferedImage(width, height, type);
}
/** Return a cached instanceof a resource, or null if no cached instance
* exists.
*/
private Object get(Type type,long id) {
Map<Long, SortedSet<Record>> group = resources.get(type);
SortedSet<Record> records = group==null ? null : group.get(id);
if(records!=null && records.size()>0) {
Iterator<Record> iter = records.iterator();
Record r = iter.next();
iter.remove();
//System.out.println("getting "+type+", "+id+", "+r.resource);
return r.resource;
}
//System.out.println("missing "+type+", "+id);
return null;
}
protected synchronized void finalize() {
resources.clear();
}
/** Return an int array from this pool. If no cached
* int array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized int[] getIntArray(int length) {
int[] cachedValue = (int[])get( Type.INT, length);
if(cachedValue!=null) return cachedValue;
return new int[length];
}
/** Return a float array from this pool. If no cached
* float array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized float[] getFloatArray(int length) {
float[] cachedValue = (float[])get( Type.FLOAT, length);
if(cachedValue!=null) return cachedValue;
return new float[length];
}
/** Return a double array from this pool. If no cached
* double array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized double[] getDoubleArray(int length) {
double[] cachedValue = (double[])get( Type.DOUBLE, length);
if(cachedValue!=null) return cachedValue;
return new double[length];
}
/** Return a short array from this pool. If no cached
* short array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized short[] getShortArray(int length) {
short[] cachedValue = (short[])get( Type.SHORT, length);
if(cachedValue!=null) return cachedValue;
return new short[length];
}
/** Return a long array from this pool. If no cached
* long array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized long[] getLongArray(int length) {
long[] cachedValue = (long[])get( Type.LONG, length);
if(cachedValue!=null) return cachedValue;
return new long[length];
}
/** Return a byte array from this pool. If no cached
* byte array exists matching the requested length: then a new array
* is created.
* @param length the array length
*/
public synchronized byte[] getByteArray(int length) {
byte[] cachedValue = (byte[])get( Type.BYTE, length);
if(cachedValue!=null) return cachedValue;
return new byte[length];
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(long[] array) {
return store( Type.LONG, array.length, array);
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(float[] array) {
return store( Type.FLOAT, array.length, array);
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(int[] array) {
return store( Type.INT, array.length, array);
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(byte[] array) {
return store( Type.BYTE, array.length, array);
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(short[] array) {
return store( Type.SHORT, array.length, array);
}
/** Store an array in this pool for future reuse.
* @return true if the array was stored, false if
* this pool is not currently accepting any more
* arrays of this type/size.
*/
public synchronized boolean put(double[] array) {
return store( Type.DOUBLE, array.length, array);
}
/** Store an image in this pool for future reuse.
* @return true if the image was stored, false if
* this pool is not currently accepting any more
* images of this type/size.
*/
public synchronized boolean put(BufferedImage bi) {
long id = (bi.getWidth() << 16) + (bi.getHeight() << 8) + bi.getType();
return store( Type.IMAGE, id, bi);
}
/** Return the total number of resources stored in this pool of this type. */
private int getSize(Type type) {
int totalSize = 0;
Map<Long, SortedSet<Record>> group = resources.get(type);
for(Long id : group.keySet()) {
SortedSet<Record> cluster = group.get(id);
totalSize += cluster.size();
}
return totalSize;
}
/** Store a resource in this pool.
*
* @return true if the resource was stored, false if the current
* Limits do not allow storing more objects of this type/id
*/
private boolean store(Type type,long id,Object resource) {
Map<Long, SortedSet<Record>> group = resources.get(type);
Limit limit = getLimit(type);
if(group==null) {
group = new HashMap<Long, SortedSet<Record>>();
resources.put(type, group);
}
SortedSet<Record> records = group.get(id);
if(records==null) {
records = new TreeSet<Record>(recordComparator);
group.put(id, records);
}
if(records.size()+1>limit.groupLimit) {
//System.out.println("abandoning "+type+", "+id+", "+resource);
return false;
}
int totalSize = getSize(type);
if(totalSize+1>limit.totalLimit) {
//System.out.println("abandoning "+type+", "+id+", "+resource);
return false;
}
//System.out.println("storing "+type+", "+id+", "+resource);
records.add(new Record(resource));
return true;
}
}