XanderCat/Pool

From Robowiki
Jump to navigation Jump to search

This page explores ideas on how to create reusable pools of objects to avoid excessive use of new resulting in excessive garbage collection that eats into robot execution time causing skipped turns.

I had an initial idea on how to do this, which I kind of like, but I also kind of dislike because it would be easy to mess up. The idea was to create an interface Poolable for any class that will participate in the pool, and then create a Pool class that manages the pools. Here is what I have so far:

The Poolable Interface

/**
 * Implement by any class that will be in the Pool.  Such classes should also have all
 * constructors removed and replaced by initialization methods (by convention, the 
 * initialization methods should be named "renew" and should return itself).  
 * Poolable classes should also have an empty argument constructor.
 * 
 * @author Scott Arnold
 */
public interface Poolable {

	/**
	 * This method should re-initialize any contained variables not initialized by a call to renew,
	 * release any other contained Poolable objects for which it created or called keep on,
	 * and set to null any contained Poolable objects to avoid dangling references.
	 */
	public void release();
}

The Pool Class

/**
 * Class for managing reusable object pools.  The primary purpose of this is to help minimize the creation of 
 * new objects to lessen the work of the garbage collector, as the garbage collector otherwise steals too much time
 * from running robots and causes missed turns.
 * 
 * @author Scott Arnold
 */
public class Pool {

	private static final Map<Class<? extends Poolable>, List<Poolable>> pools 
		= new HashMap<Class<? extends Poolable>, List<Poolable>>();
	private static List<Poolable> pool;
	private static final Log log = Logger.getLog(Pool.class);
	private static Map<Poolable, List<Object>> keepers = new HashMap<Poolable, List<Object>>();
	private static List<Object> keeperList;
	
	static {
		// for starters, we will pool snapshots, bullet shadows, and waves
		@SuppressWarnings("unchecked")
		Class<? extends Poolable>[] poolables = new Class[] {
				Snapshot.class, BulletShadow.class, Wave.class
		};
		for (Class<? extends Poolable> poolable : poolables) {
			pools.put(poolable, new ArrayList<Poolable>());
		}
	}
	
	/**
	 * Gets an unused object of the given class from the pool.  A new instance of the object is created
	 * if there are no available objects in the pool.
	 * 
	 * @param <T>
	 * @param keeper
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> T get(Object keeper, Class<T> clazz) {
		pool = pools.get(clazz);
		if (pool == null) {
			throw new IllegalArgumentException(clazz.getName() + " is not a class managed by the Pool!");
		}
		if (pool.size() == 0) {
			try {
				Poolable poolable = (Poolable)clazz.newInstance();
				pool.add(poolable);
				keepers.put(poolable, new ArrayList<Object>());
			} catch (Exception e) {
				log.error(e.getMessage());
			} 
		}
		T instance = (T) pool.remove(pool.size()-1);
		keep(keeper, (Poolable)instance);
		return instance;
	}
	
	/**
	 * Classes can call this method when they wish to hang onto a Poolable but are not creating it new.
	 * Such classes should make sure that release is called on the Poolable when it is no longer needed.
	 * 
	 * @param keeper
	 * @param poolable
	 */
	public static void keep(Object keeper, Poolable poolable) {
		keeperList = keepers.get(poolable);
		if (keeperList == null) {
			throw new IllegalArgumentException("Poolable " + poolable.toString() + " is not being managed by the pool!");
		}
		keeperList.add(keeper);
	}
	
	/**
	 * Informs the pool that the given keeper no longer needs the given poolable.  If there are no remaining
	 * keepers of the poolable, the poolable is released and returned to the pool.
	 * 
	 * @param keeper
	 * @param poolable
	 */
	public static void release(Object keeper, Poolable poolable) {
		keeperList = keepers.get(poolable);
		if (keeperList == null) {
			throw new IllegalArgumentException("Poolable " + poolable.toString() + " is not being managed by the pool!");
		}
		keeperList.remove(keeper);
		if (keeperList.size() == 0) {
			pool = pools.get(poolable.getClass());
			if (pool == null) {
				throw new IllegalArgumentException(poolable.getClass().getName() + " is not a class managed by the Pool!");
			}
			poolable.release();
			pool.add(poolable);
		}
	}
}

How It's Used

As the javadocs state, an object that will participate in a pool implements the Poolable interface. Also, as a convention, that class should be changed to have renew(...) methods that replace the constructors and return itself, and have a default constructor that is used by the Pool to create new instances when needed.

Any class that wants to create a new object that is a pooled object calls get(...) on the Pool instead of just creating a new object. That class must ensure that release(...) gets called on the object when done with it.

Any class that wants to hang onto a pooled object but is not creating it new can call keep(...) on the Pool. That class must ensure that release(...) gets called on the object when done with it.

Whether calling get(...) or keep(...), the calling object gets added as a "keeper". The Pool maintains a list of keepers for each poolable object removed from the pool. When all keepers have released the object, the object is returned to the pool.

Example of Getting Object From Pool

In this example, a poolable class Snapshot is being obtained from the Pool. Before it was Poolable, Snapshot had a constructor that took 2 arguments, a RobotProxy object and a double.

Without Pools

Snapshot mySnapshot = new Snapshot(robotProxy, 0);

With Pools

Snapshot mySnapshot = Pool.get(this, Snapshot.class).renew(robotProxy, 0);

Keeping An Existing Poolable

// somehow either mySnapshot is looked up or passed in...
Pool.keep(this, mySnapshot);

Releasing a Poolable

// we are done with the Poolable object that we previously either called get(...) or keep(...) on
Pool.release(this, mySnapshot);

Problems/Concerns

  • Rules are not well enforced; much of the framework must be adhered to out of convention (renew methods, proper use of release method implemented by Poolable classes.)
  • The code has to just know what objects are poolable. This is especially difficult when the poolable is just passed to another class via a parameter or looked up from somewhere. The code has to know the object is poolable and remember to call keep(...) on it.
  • It would be very easy to forget to call release().
  • Additional diagnostic code will need to be added to debug any problems.

Projected Savings in XanderCat

Class: Snapshot Approximate instances per round: 2 snapshots per tick (one for self, one for scanned opponent) x typical round of 700 ticks = 1400 instances

Class: Wave and XBulletWave Approximate instances per round: approx 45 shots per round, 45 waves for opponent, 2 guns with virtual waves in XanderCat, total = 135 instances

Class: BulletShadow Approximate instances per round: approx 90 waves per round x approx 3 shadows per wave = 270 instances