/* Copyright (C) 2005-2011 Fabio Riccardi */
/*
* $RCSfile: SunTileCache.java,v $
*
* Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
*
* Use is subject to license terms.
*
* $Revision: 1.1 $
* $Date: 2005/02/11 04:57:02 $
* $State: Exp $
*/
package com.lightcrafts.jai.utils;
import com.lightcrafts.media.jai.util.CacheDiagnostics;
import com.lightcrafts.media.jai.util.ImageUtil;
import com.lightcrafts.utils.cache.*;
import com.lightcrafts.utils.MemoryLimits;
import com.lightcrafts.utils.LCArrays;
import com.lightcrafts.jai.JAIContext;
import java.awt.RenderingHints;
import java.util.*;
import java.util.prefs.Preferences;
import java.awt.Point;
import java.awt.image.*;
import java.io.*;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.ref.Reference;
import java.nio.ByteBuffer;
import com.lightcrafts.mediax.jai.EnumeratedParameter;
import com.lightcrafts.mediax.jai.TileCache;
import com.lightcrafts.mediax.jai.util.ImagingListener;
/**
* This is Sun Microsystems' reference implementation of the
* <code>com.lightcrafts.mediax.jai.TileCache</code> interface. It provides a
* central location for images to cache computed tiles, and is used as
* the default tile cache mechanism when no other tile cache objects
* are specified.
*
* <p> In this implementation, the cache size is limited by the memory
* capacity, which may be set at construction time or using the
* <code>setMemoryCapacity(long)</code> method. The tile capacity
* is not used. Different images may have very different tile sizes.
* Therefore, the memory usage for a specific tile capacity may vary
* greatly depends on the type of images involved. In fact, the tile
* capacity is rather meaningless.
*
* @see com.lightcrafts.mediax.jai.TileCache
*
*/
public final class LCTileCache extends Observable
implements TileCache,
CacheDiagnostics {
/** The default memory capacity of the cache (16 MB). */
private static final long DEFAULT_MEMORY_CAPACITY = 16L * 1024L * 1024L;
/** The default hashtable capacity (heuristic) */
private static final int DEFAULT_HASHTABLE_CAPACITY = 1009; // prime number
/** The hashtable load factor */
private static final float LOAD_FACTOR = 0.5F;
/** Listener for the flush() method, to detect low memory situations. */
private static LCTileCacheListener Listener;
/**
* The tile cache.
* A Hashtable is used to cache the tiles. The "key" is a
* <code>Object</code> determined based on tile owner's UID if any or
* hashCode if the UID doesn't exist, and tile index. The
* "value" is a LCCachedTile.
*/
private Hashtable cache;
/**
* Sorted (Tree) Set used with tile metrics.
* Adds another level of metrics used to determine
* which tiles are removed during memoryControl().
*/
private SortedSet cacheSortedSet;
/** The memory capacity of the cache. */
private long memoryCapacity;
/** The amount of memory currently being used by the cache. */
private long memoryUsage = 0;
/** The amount of memory to keep after memory control */
private float memoryThreshold = 0.75F;
/** A indicator for tile access time. */
private long timeStamp = 0;
/** Custom comparator used to determine tile cost or
* priority ordering in the tile cache.
*/
private Comparator comparator = null;
/** Pointer to the first (newest) tile of the linked LCCachedTile list. */
private LCCachedTile first = null;
/** Pointer to the last (oldest) tile of the linked LCCachedTile list. */
private LCCachedTile last = null;
/** Tile count used for diagnostics */
private long tileCount = 0;
/** Cache hit count */
private long hitCount = 0;
/** Cache miss count */
private long missCount = 0;
/** Diagnostics enable/disable */
private boolean diagnostics;
private Cache m_objectCache;
// diagnostic actions
// !!! If actions are changed in any way (removal, modification, addition)
// then the getCachedTileActions() method below should be changed to match.
private static final int ADD = 0;
private static final int REMOVE = 1;
private static final int REMOVE_FROM_FLUSH = 2;
private static final int REMOVE_FROM_MEMCON = 3;
private static final int UPDATE_FROM_ADD = 4;
private static final int UPDATE_FROM_GETTILE = 5;
private static final int ABOUT_TO_REMOVE = 6;
private static final int REMOVE_FROM_GCEVENT = 7;
/**
* Returns an array of <code>EnumeratedParameter</code>s corresponding
* to the numeric values returned by the <code>getAction()</code>
* method of the <code>CachedTile</code> implementation used by
* <code>SunTileCache</code>. The "name" of each
* <code>EnumeratedParameter</code> provides a brief string
* describing the numeric action value.
*/
public static EnumeratedParameter[] getCachedTileActions() {
return new EnumeratedParameter[] {
new EnumeratedParameter("add", ADD),
new EnumeratedParameter("remove", REMOVE),
new EnumeratedParameter("remove_by_flush", REMOVE_FROM_FLUSH),
new EnumeratedParameter("remove_by_memorycontrol",
REMOVE_FROM_MEMCON),
new EnumeratedParameter("remove_by_gcevent",
REMOVE_FROM_GCEVENT),
new EnumeratedParameter("timestamp_update_by_add", UPDATE_FROM_ADD),
new EnumeratedParameter("timestamp_update_by_gettile",
UPDATE_FROM_GETTILE),
new EnumeratedParameter("preremove", ABOUT_TO_REMOVE)
};
}
/** Get callbacks about calls to flush(), to detect low memory. */
public static synchronized void setListener(LCTileCacheListener listener) {
Listener = listener;
}
/**
* No args constructor. Use the DEFAULT_MEMORY_CAPACITY of 16 Megs.
*/
public LCTileCache(boolean useDisk) {
this(DEFAULT_MEMORY_CAPACITY, useDisk);
}
/**
* Constructor. The memory capacity should be explicitly specified.
*
* @param memoryCapacity The maximum cache memory size in bytes.
*
* @throws IllegalArgumentException If <code>memoryCapacity</code>
* is less than 0.
*/
public LCTileCache(long memoryCapacity, boolean useDisk) {
if (memoryCapacity < 0) {
throw new IllegalArgumentException("memory capacity must be >= 0");
}
this.memoryCapacity = memoryCapacity;
// try to get a prime number (more efficient?)
// lower values of LOAD_FACTOR increase speed, decrease space efficiency
cache = new Hashtable(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
if (useDisk) {
m_objectCache = createDiskCache();
m_tileReaper = new TileReaper( this );
m_tileReaper.start();
}
}
/**
* Adds a tile to the cache.
*
* <p> If the specified tile is already in the cache, it will not be
* cached again. If by adding this tile, the cache exceeds the memory
* capacity, older tiles in the cache are removed to keep the cache
* memory usage under the specified limit.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
* @param tile The tile to be cached.
*/
public void add(RenderedImage owner,
int tileX,
int tileY,
Raster tile) {
add(owner, tileX, tileY, tile, null);
}
/**
* Adds a tile to the cache with an associated tile compute cost.
*
* <p> If the specified tile is already in the cache, it will not be
* cached again. If by adding this tile, the cache exceeds the memory
* capacity, older tiles in the cache are removed to keep the cache
* memory usage under the specified limit.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
* @param tile The tile to be cached.
* @param tileCacheMetric Metric for prioritizing tiles
*/
public synchronized void add(RenderedImage owner,
int tileX,
int tileY,
Raster tile,
Object tileCacheMetric) {
if ( memoryCapacity == 0 ) {
return;
}
// This tile is not in the cache; create a new LCCachedTile.
// else just update.
Object key = LCCachedTile.hashKey(owner, tileX, tileY);
LCCachedTile ct = (LCCachedTile) cache.get(key);
if ( ct != null ) {
updateTileList(ct, UPDATE_FROM_ADD);
} else {
// create a new tile
ct = new LCCachedTile(owner, tileX, tileY, tile, tileCacheMetric);
ct.timeStamp = timeStamp++;
ct.previous = null;
ct.next = first;
if (first == null && last == null) {
first = ct;
last = ct;
} else {
first.previous = ct;
first = ct; // put this tile at the top of the list
}
// add to tile cache
if ( cache.put(ct.key, ct) == null ) {
memoryUsage += ct.memorySize;
tileCount++;
//missCount++; Not necessary?
if ( cacheSortedSet != null ) {
cacheSortedSet.add(ct);
}
if ( diagnostics ) {
ct.action = ADD;
setChanged();
notifyObservers(ct);
}
}
// Bring memory usage down to memoryThreshold % of memory capacity.
if (memoryUsage > memoryCapacity) {
memoryControl();
}
}
if (m_tileReaper != null) {
// TODO: do we need this?
synchronized ( this ) {
WeakReference weakKey = null;
Set keySet = m_imageMap.keySet();
Iterator it = keySet.iterator();
while (it.hasNext()) {
WeakReference ref = (WeakReference) it.next();
if (ref.get() == owner) {
weakKey = ref;
break;
}
}
// weakKey = (WeakReference) m_weakRefMap.get(owner);
if (weakKey == null) {
weakKey = new WeakReference( owner, m_tileReaper.getRefQ() );
// m_weakRefMap.put(owner, weakKey);
}
Set hashKeys = (HashSet) m_imageMap.get(weakKey);
if (hashKeys == null) {
hashKeys = new HashSet();
m_imageMap.put(weakKey, hashKeys);
}
hashKeys.add(key);
}
}
}
private boolean removeFromTileList(Object key, int action) {
LCCachedTile ct = (LCCachedTile) cache.remove(key);
if (ct != null) {
memoryUsage -= ct.memorySize;
tileCount--;
if ( cacheSortedSet != null ) {
cacheSortedSet.remove(ct);
}
if ( ct == first ) {
if ( ct == last ) {
first = null; // only one tile in the list
last = null;
} else {
first = ct.next;
first.previous = null;
}
} else if ( ct == last ) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
// Notify observers that a tile has been removed.
if ( diagnostics ) {
ct.action = action;
setChanged();
notifyObservers(ct);
}
ct.previous = null;
ct.next = null;
ct = null;
return true;
}
return false;
}
private void updateTileList(LCCachedTile ct, int action) {
ct.timeStamp = timeStamp++;
if (ct != first) {
// Bring this tile to the beginning of the list.
if (ct == last) {
last = ct.previous;
last.next = null;
} else {
ct.previous.next = ct.next;
ct.next.previous = ct.previous;
}
ct.previous = null;
ct.next = first;
first.previous = ct;
first = ct;
}
hitCount++;
if ( diagnostics ) {
ct.action = action;
setChanged();
notifyObservers(ct);
}
}
/**
* Removes a tile from the cache.
*
* <p> If the specified tile is not in the cache, this method
* does nothing.
*/
public synchronized void remove(RenderedImage owner,
int tileX,
int tileY) {
if ( memoryCapacity == 0 ) {
return;
}
Object key = LCCachedTile.hashKey(owner, tileX, tileY);
LCCachedTile ct = (LCCachedTile) cache.get(key);
if ( ct != null ) {
// Notify observers that a tile is about to be removed.
// It is possible that the tile will be removed from the
// cache before the observers get notified. This should
// be ok, since a hard reference to the tile will be
// kept for the observers, so the garbage collector won't
// remove the tile until the observers release it.
ct.action = ABOUT_TO_REMOVE;
setChanged();
notifyObservers(ct);
removeFromTileList(key, REMOVE);
} else {
// if the tile is not in the memory cache than it might be on disk...
if (m_objectCache != null && m_objectCache.contains(key)) {
m_objectCache.remove(key);
tilesOnDisk--;
}
}
}
/**
* Retrieves a tile from the cache.
*
* <p> If the specified tile is not in the cache, this method
* returns <code>null</code>. If the specified tile is in the
* cache, its last-access time is updated.
*
* @param owner The image the tile blongs to.
* @param tileX The tile's X index within the image.
* @param tileY The tile's Y index within the image.
*/
public synchronized Raster getTile(RenderedImage owner,
int tileX,
int tileY) {
if ( memoryCapacity == 0 )
return null;
Object key = LCCachedTile.hashKey(owner, tileX, tileY);
LCCachedTile ct = (LCCachedTile) cache.get(key);
if (m_objectCache != null && ct == null) {
Raster raster = readTileFromDisk(owner, tileX, tileY, key);
if (raster != null) {
add(owner, tileX, tileY, raster, null);
ct = (LCCachedTile) cache.get(key);
assert ct != null;
}
}
Raster tile = null;
if ( ct == null ) {
missCount++;
} else { // found tile in cache
tile = (Raster) ct.getTile();
updateTileList(ct, UPDATE_FROM_GETTILE);
}
return tile;
}
/**
* Retrieves a contiguous array of all tiles in the cache which are
* owned by the specified image. May be <code>null</code> if there
* were no tiles in the cache. The array contains no null entries.
*
* @param owner The <code>RenderedImage</code> to which the tiles belong.
* @return An array of all tiles owned by the specified image or
* <code>null</code> if there are none currently in the cache.
*/
public synchronized Raster[] getTiles(RenderedImage owner) {
Raster[] tiles = null;
if ( memoryCapacity == 0 ) {
return null;
}
int size = Math.min(owner.getNumXTiles() * owner.getNumYTiles(),
(int)tileCount);
if ( size > 0 ) {
int minTx = owner.getMinTileX();
int minTy = owner.getMinTileY();
int maxTx = minTx + owner.getNumXTiles();
int maxTy = minTy + owner.getNumYTiles();
// arbitrarily set a temporary vector size
Vector temp = new Vector(10, 20);
for (int y = minTy; y < maxTy; y++) {
for (int x = minTx; x < maxTx; x++) {
Raster raster = getTile(owner, x, y);
if ( raster != null ) {
temp.add(raster);
}
}
}
int tmpsize = temp.size();
if ( tmpsize > 0 ) {
tiles = (Raster[])temp.toArray(new Raster[tmpsize]);
}
}
return tiles;
}
/**
* Removes all the tiles that belong to a <code>RenderedImage</code>
* from the cache.
*
* @param owner The image whose tiles are to be removed from the cache.
*/
public void removeTiles(RenderedImage owner) {
if ( memoryCapacity > 0 ) {
int minTx = owner.getMinTileX();
int minTy = owner.getMinTileY();
int maxTx = minTx + owner.getNumXTiles();
int maxTy = minTy + owner.getNumYTiles();
for (int y=minTy; y<maxTy; y++) {
for (int x=minTx; x<maxTx; x++) {
remove(owner, x, y);
}
}
}
}
/**
* Adds an array of tiles to the tile cache.
*
* @param owner The <code>RenderedImage</code> that the tile belongs to.
* @param tileIndices An array of <code>Point</code>s containing the
* <code>tileX</code> and <code>tileY</code> indices for each tile.
* @param tiles The array of tile <code>Raster</code>s containing tile data.
* @param tileCacheMetric Object which provides an ordering metric
* associated with the <code>RenderedImage</code> owner.
* @since 1.1
*/
public synchronized void addTiles(RenderedImage owner,
Point[] tileIndices,
Raster[] tiles,
Object tileCacheMetric) {
if ( memoryCapacity == 0 ) {
return;
}
for ( int i = 0; i < tileIndices.length; i++ ) {
int tileX = tileIndices[i].x;
int tileY = tileIndices[i].y;
Raster tile = tiles[i];
add(owner, tileX, tileY, tile, tileCacheMetric);
}
}
/**
* Returns an array of tile <code>Raster</code>s from the cache.
* Any or all of the elements of the returned array may be <code>null</code>
* if the corresponding tile is not in the cache.
*
* @param owner The <code>RenderedImage</code> that the tile belongs to.
* @param tileIndices An array of <code>Point</code>s containing the
* <code>tileX</code> and <code>tileY</code> indices for each tile.
* @since 1.1
*/
public synchronized Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
if ( memoryCapacity == 0 ) {
return null;
}
Raster[] tiles = new Raster[tileIndices.length];
for ( int i = 0; i < tiles.length; i++ ) {
int tileX = tileIndices[i].x;
int tileY = tileIndices[i].y;
tiles[i] = getTile(owner, tileX, tileY);
}
return tiles;
}
/** Removes -ALL- tiles from the cache. */
public synchronized void flush() {
// Call the LCTileCacheListener, if one is defined. This helps detect
// low memory conditions.
if (Listener != null) {
Listener.tileCacheFlushed();
}
// NOTE: we don't do flushing for disk caches, it wipes the persistent cache, rather spill half of the cache out
if (m_objectCache != null) {
System.err.println("flushing the cache");
float mt = memoryThreshold;
memoryThreshold = 0.1f;
memoryControl();
memoryThreshold = mt;
return;
}
//
// It is necessary to clear all the elements
// from the old cache in order to remove dangling
// references, due to the linked list. In other
// words, it is possible to reache the object
// through 2 paths so the object does not
// become weakly reachable until the reference
// to it in the hash map is null. It is not enough
// to just set the object to null.
//
Enumeration keys = cache.keys(); // all keys in Hashtable
// reset counters before diagnostics
hitCount = 0;
missCount = 0;
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
removeFromTileList(key, REMOVE_FROM_FLUSH);
}
if ( memoryCapacity > 0 ) {
cache = new Hashtable(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
}
if ( cacheSortedSet != null ) {
cacheSortedSet.clear();
cacheSortedSet = Collections.synchronizedSortedSet( new TreeSet(comparator) );
}
// force reset after diagnostics
tileCount = 0;
timeStamp = 0;
memoryUsage = 0;
// no System.gc() here, it's too slow and may occur anyway.
}
/**
* Returns the cache's tile capacity.
*
* <p> This implementation of <code>TileCache</code> does not use
* the tile capacity. This method always returns 0.
*/
public int getTileCapacity() { return 0; }
/**
* Sets the cache's tile capacity to the desired number of tiles.
*
* <p> This implementation of <code>TileCache</code> does not use
* the tile capacity. The cache size is limited by the memory
* capacity only. This method does nothing and has no effect on
* the cache.
*
* @param tileCapacity The desired tile capacity for this cache
* in number of tiles.
*/
public void setTileCapacity(int tileCapacity) { }
/** Returns the cache's memory capacity in bytes. */
public long getMemoryCapacity() {
return memoryCapacity;
}
/**
* Sets the cache's memory capacity to the desired number of bytes.
* If the new memory capacity is smaller than the amount of memory
* currently being used by this cache, tiles are removed from the
* cache until the memory usage is less than the specified memory
* capacity.
*
* @param memoryCapacity The desired memory capacity for this cache
* in bytes.
*
* @throws IllegalArgumentException If <code>memoryCapacity</code>
* is less than 0.
*/
public void setMemoryCapacity(long memoryCapacity) {
if (memoryCapacity < 0) {
throw new IllegalArgumentException("memory capacity must be >= 0");
} else if ( memoryCapacity == 0 ) {
flush();
}
this.memoryCapacity = memoryCapacity;
if ( memoryUsage > memoryCapacity ) {
memoryControl();
}
}
/** Enable Tile Monitoring and Diagnostics */
public void enableDiagnostics() {
diagnostics = true;
}
/** Turn off diagnostic notification */
public void disableDiagnostics() {
diagnostics = false;
}
public long getCacheTileCount() {
return tileCount;
}
public long getCacheMemoryUsed() {
return memoryUsage;
}
public long getCacheHitCount() {
return hitCount;
}
public long getCacheMissCount() {
return missCount;
}
/**
* Reset hit and miss counters.
*
* @since 1.1
*/
public void resetCounts() {
hitCount = 0;
missCount = 0;
}
/**
* Set the memory threshold value.
*
* @since 1.1
*/
public void setMemoryThreshold(float mt) {
if ( mt < 0.0F || mt > 1.0F ) {
throw new IllegalArgumentException("memory threshold must be between 0 and 1");
} else {
memoryThreshold = mt;
memoryControl();
}
}
/**
* Returns the current <code>memoryThreshold</code>.
*
* @since 1.1
*/
public float getMemoryThreshold() {
return memoryThreshold;
}
/** Returns a string representation of the class object. */
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode()) +
": memoryCapacity = " + Long.toHexString(memoryCapacity) +
" memoryUsage = " + Long.toHexString(memoryUsage) +
" #tilesInCache = " + Integer.toString(cache.size());
}
/** Returns the <code>Object</code> that represents the actual cache. */
public Object getCachedObject() {
return cache;
}
/**
* Removes tiles from the cache based on their last-access time
* (old to new) until the memory usage is memoryThreshold % of that of the
* memory capacity.
*/
public synchronized void memoryControl() {
if ( cacheSortedSet == null ) {
standard_memory_control();
} else {
custom_memory_control();
}
}
// time stamp based memory control (LRU)
private final void standard_memory_control() {
long limit = (long)(memoryCapacity * memoryThreshold);
while( memoryUsage > limit && last != null ) {
LCCachedTile ct = (LCCachedTile) cache.get(last.key);
if ( ct != null ) {
RenderedImage owner = ct.getOwner();
if (owner != null && owner.getProperty(JAIContext.PERSISTENT_CACHE_TAG) == Boolean.TRUE)
if (m_objectCache != null)
writeTileToDisk(ct, last.key);
removeFromTileList(last.key, REMOVE_FROM_MEMCON);
}
}
}
private static class TileCacheCacheObjectBroker implements CacheObjectBroker {
public int getEncodedSizeOf( Object obj ) {
if ( obj instanceof byte[] ) {
final byte[] ba = (byte[])obj;
return ba.length;
} else if ( obj instanceof short[] ) {
final short[] sa = (short[])obj;
return sa.length * 2;
} else if ( obj instanceof int[] ) {
final int[] ia = (int[])obj;
return ia.length * 4;
} else
throw new IllegalArgumentException(
"can't get size of " + obj.getClass()
);
}
// TODO: cache the ByteBuffer with a soft reference
public Object decodeFromByteBuffer( ByteBuffer buf, Object obj ) {
if ( obj instanceof byte[] )
//buf.get( (byte[])obj );
LCArrays.copy( buf.array(), 0, (byte[])obj, 0, buf.capacity() );
else if ( obj instanceof short[] )
//buf.asShortBuffer().get( (short[])obj );
LCArrays.copy( buf.array(), 0, (short[])obj, 0, buf.capacity() );
else if ( obj instanceof int[] )
//buf.asIntBuffer().get( (int[])obj );
LCArrays.copy( buf.array(), 0, (int[])obj, 0, buf.capacity() );
else
throw new IllegalArgumentException(
"can't decode " + obj.getClass()
);
return obj;
}
public void encodeToByteBuffer( ByteBuffer buf, Object obj ) {
if ( obj instanceof byte[] )
//buf.put( (byte[])obj );
LCArrays.copy( (byte[])obj, 0, buf.array(), 0, buf.capacity() );
else if ( obj instanceof short[] )
//buf.asShortBuffer().put( (short[])obj );
LCArrays.copy( (short[])obj, 0, buf.array(), 0, buf.capacity() );
else if ( obj instanceof int[] )
//buf.asIntBuffer().put( (int[])obj );
LCArrays.copy( (int[])obj, 0, buf.array(), 0, buf.capacity() );
else
throw new IllegalArgumentException(
"can't encode " + obj.getClass()
);
}
}
static class CacheFileFilter implements FilenameFilter {
File goodFile;
CacheFileFilter(File goodFile) {
this.goodFile = goodFile;
}
public boolean accept(File dir, String name) {
if (!name.equals(goodFile.getName()) && name.startsWith("LCCacheFile") && name.endsWith(".cce"))
return true;
return false;
}
}
private final static Preferences Prefs =
Preferences.userNodeForPackage(LCTileCache.class);
private final static String CacheDirKey = "ScratchDirectory";
private File tmpFile = null;
private Cache createDiskCache() {
try {
// Try creating the temp file in the user-specified location
String path = Prefs.get(CacheDirKey, null);
tmpFile = null;
if (path != null) {
File tmpDir = new File(path);
if (tmpDir.isDirectory() && tmpDir.canWrite()) {
tmpFile = File.createTempFile("LCCacheFile", ".cce", tmpDir);
}
}
// Fall back to the regular java temp directory
if (tmpFile == null) {
tmpFile = File.createTempFile("LCCacheFile", ".cce");
}
tmpFile.deleteOnExit();
// Try to delete old cache files, checking if the file is locked by some other instance of ourself
File[] oldCacheFiles = tmpFile.getParentFile().listFiles(new CacheFileFilter(tmpFile));
if ( oldCacheFiles != null )
for ( int i = 0; i < oldCacheFiles.length; i++ )
oldCacheFiles[i].delete();
int defaultMemorySize = MemoryLimits.getDefault();
Preferences prefs = Preferences.userRoot().node("/com/lightcrafts/app");
long maxMemory = (long) prefs.getInt("MaxMemory", defaultMemorySize) * 1024 * 1024;
long maxHeap = Runtime.getRuntime().maxMemory();
long extraCaheSize = (maxMemory - maxHeap)/(1024 * 1024);
System.out.println("Allocating " + extraCaheSize + "MB for the image cache.");
return new Cache(
new TileCacheCacheObjectBroker(),
extraCaheSize < 128 * 1024 * 1024 ?
new WriteThroughCacheObjectMap() :
new LRUCacheObjectMap(
new NativeByteBufferAllocator( CHUNK_SIZE ), extraCaheSize
),
new DirectFileCacheStore( tmpFile ),
new CoalescingFreeBlockManager()
);
}
catch ( IOException e ) {
e.printStackTrace();
return null;
}
}
// private static final long CACHE_SIZE = (long) (1024 * 1024 * 1024);
private static final int CHUNK_SIZE = 16 * 1024 * 1024;
public synchronized void dispose() throws IOException {
m_objectCache.dispose();
// Close and delete the old cache file
if (m_tileReaper != null)
m_tileReaper.kill();
if (tmpFile != null)
tmpFile.delete();
}
/**
* Finalize an <code>LCTileCache</code>.
*/
protected void finalize() throws Throwable {
dispose();
super.finalize();
}
private long tilesWritten = 0;
private long tilesRead = 0;
private long tilesOnDisk = 0;
public long tilesWritten() {
return tilesWritten;
}
public long tilesRead() {
return tilesRead;
}
public long tilesOnDisk() {
return tilesOnDisk;
}
private Raster readTileFromDisk(RenderedImage owner, int tileX, int tileY, Object key) {
if (m_objectCache.contains(key)) {
SampleModel sm = owner.getSampleModel();
DataBuffer db = sm.createDataBuffer();
try {
switch (db.getDataType()) {
case DataBuffer.TYPE_BYTE:
m_objectCache.getOnce(key, ((DataBufferByte) db).getData());
break;
case DataBuffer.TYPE_USHORT:
m_objectCache.getOnce(key, ((DataBufferUShort) db).getData());
break;
case DataBuffer.TYPE_INT:
m_objectCache.getOnce(key, ((DataBufferInt) db).getData());
break;
default:
throw new IllegalArgumentException("unsupported image type " + db.getClass());
}
synchronized (this) {
tilesOnDisk--;
}
} catch (IOException e) {
e.printStackTrace();
}
WritableRaster raster;
if (true)
raster = Raster.createWritableRaster(sm, db, new Point(tileX * owner.getTileWidth(),
tileY * owner.getTileHeight()));
else {
int bands = sm.getNumBands();
int bandOffsets[] = ((PixelInterleavedSampleModel) sm).getBandOffsets();
raster = Raster.createInterleavedRaster(db, owner.getTileWidth(), owner.getTileHeight(),
bands * owner.getTileWidth(), bands, bandOffsets,
new Point(tileX * owner.getTileWidth(),
tileY * owner.getTileHeight()));
}
synchronized (this) {
tilesRead++;
}
return raster;
} else
return null;
}
private void writeTileToDisk(LCCachedTile ct, Object key) {
Raster raster = ct.getTile();
DataBuffer db = raster.getDataBuffer();
try {
switch (db.getDataType()) {
case DataBuffer.TYPE_BYTE:
m_objectCache.put(key, ((DataBufferByte) db).getData());
break;
case DataBuffer.TYPE_USHORT:
m_objectCache.put(key, ((DataBufferUShort) db).getData());
break;
case DataBuffer.TYPE_INT:
m_objectCache.put(key, ((DataBufferInt) db).getData());
break;
default:
throw new IllegalArgumentException("unsupported image type " + db.getClass());
}
synchronized (this) {
tilesOnDisk++;
}
} catch (IOException e) {
e.printStackTrace();
}
synchronized (this) {
tilesWritten++;
}
}
// comparator based memory control (TreeSet)
private final void custom_memory_control() {
long limit = (long)(memoryCapacity * memoryThreshold);
Iterator iter = cacheSortedSet.iterator();
LCCachedTile ct;
while( iter.hasNext() && (memoryUsage > limit) ) {
ct = (LCCachedTile) iter.next();
memoryUsage -= ct.memorySize;
synchronized (this) {
tileCount--;
}
// remove from sorted set
try {
iter.remove();
} catch(ConcurrentModificationException e) {
ImagingListener listener =
ImageUtil.getImagingListener((RenderingHints)null);
listener.errorOccurred("something wrong with the TileCache",
e, this, false);
// e.printStackTrace();
}
// remove tile from the linked list
if ( ct == first ) {
if ( ct == last ) {
first = null;
last = null;
} else {
first = ct.next;
if ( first != null ) {
first.previous = null;
first.next = ct.next.next;
}
}
} else if ( ct == last ) {
last = ct.previous;
if ( last != null ) {
last.next = null;
last.previous = ct.previous.previous;
}
} else {
LCCachedTile ptr = first.next;
while( ptr != null ) {
if ( ptr == ct ) {
if ( ptr.previous != null ) {
ptr.previous.next = ptr.next;
}
if ( ptr.next != null ) {
ptr.next.previous = ptr.previous;
}
break;
}
ptr = ptr.next;
}
}
// remove reference in the hashtable
cache.remove(ct.key);
// diagnostics
if ( diagnostics ) {
ct.action = REMOVE_FROM_MEMCON;
setChanged();
notifyObservers(ct);
}
}
// If the custom memory control didn't release sufficient
// number of tiles to satisfy the memory limit, fallback
// to the standard memory controller.
if ( memoryUsage > limit ) {
standard_memory_control();
}
}
/**
* The <code>Comparator</code> is used to produce an
* ordered list of tiles based on a user defined
* compute cost or priority metric. This determines
* which tiles are subject to "ordered" removal
* during a memory control operation.
*
* @since 1.1
*/
public synchronized void setTileComparator(Comparator c) {
if (comparator != null)
throw new IllegalArgumentException("TileComparator not supported by LCTileCache");
comparator = c;
if ( comparator == null ) {
// turn of comparator
if ( cacheSortedSet != null ) {
cacheSortedSet.clear();
cacheSortedSet = null;
}
} else {
// copy tiles from hashtable to sorted tree set
cacheSortedSet = Collections.synchronizedSortedSet( new TreeSet(comparator) );
Enumeration keys = cache.keys();
while( keys.hasMoreElements() ) {
Object key = keys.nextElement();
Object ct = cache.get(key);
cacheSortedSet.add(ct);
}
}
}
/**
* Return the current comparator
*
* @since 1.1
*/
public Comparator getTileComparator() {
return comparator;
}
// test
public void dump() {
System.out.println("first = " + first);
System.out.println("last = " + last);
Iterator iter = cacheSortedSet.iterator();
int k = 0;
while( iter.hasNext() ) {
LCCachedTile ct = (LCCachedTile) iter.next();
System.out.println(k++);
System.out.println(ct);
}
}
void sendExceptionToListener(String message, Exception e) {
ImagingListener listener =
ImageUtil.getImagingListener((RenderingHints)null);
listener.errorOccurred(message, e, this, false);
}
/**
* A <code>TileReaper</code> is-a {@link Thread} that runs continuously and
* asynchronously in the background waiting for {@link RenderedImage}s that
* the Java garbage collector has determined are weakly reachable. Once
* that's the case, remove all of a {@link RenderedImage}'s associated
* tiles from the disk cache.
*/
private static final class TileReaper extends Thread {
////////// public /////////////////////////////////////////////////////
/**
* Run the thread: wait for a weakly reachable {@link RenderedImage} to
* become available and remove all of its tiles from the disk cache
* (if any).
*/
public void run() {
while ( !m_killed ) {
try {
final Reference weakKey = m_refQ.remove(); // Image to be garbage collected
final LCTileCache tileCache =
(LCTileCache) m_tileCacheRef.get();
if ( tileCache == null )
break;
synchronized ( tileCache ) {
// System.out.println( "Removing tiles from caches" );
Set hashKeys = (Set) tileCache.m_imageMap.remove(weakKey);
assert hashKeys != null;
for ( Iterator i = hashKeys.iterator(); i.hasNext(); ) {
Object o = i.next();
if (tileCache.removeFromTileList(o, REMOVE_FROM_GCEVENT)) {
// System.out.println("removed entry from memory cache");
}
if (tileCache.m_objectCache.remove(o)) {
synchronized (tileCache) {
tileCache.tilesOnDisk--;
}
// System.out.println("removed entry from disk cache");
}
}
}
}
catch ( InterruptedException e ) {
// do nothing
}
}
}
////////// package ////////////////////////////////////////////////////
/**
* Construct a <code>TileReaper</code> and make it a daemon.
*/
TileReaper( LCTileCache tileCache ) {
super("TileReaper");
setDaemon( true );
m_refQ = new ReferenceQueue();
m_tileCacheRef = new WeakReference( tileCache );
}
/**
* Gets the {@link ReferenceQueue} being used.
*
* @return Returns said {@link ReferenceQueue}.
*/
ReferenceQueue getRefQ() {
return m_refQ;
}
/**
* Kill this thread.
*/
void kill() {
m_killed = true;
interrupt();
}
////////// private ////////////////////////////////////////////////////
/**
* A flag to indicate whether this thread has been killed.
*/
private boolean m_killed;
/**
* The {@link ReferenceQueue} wherein the Java garbage collector
* deposits objects that have become weakly reachable.
*/
private final ReferenceQueue m_refQ;
/**
* A {@link WeakReference} to the owning {@link LCTileCache}.
* TODO: explain why this is needed instead of using an inner class.
*/
private final WeakReference m_tileCacheRef;
}
/**
* This is a set of {@link WeakReference}s to {@link RenderedImage}s.
*/
private final Map m_imageMap = new HashMap();
/**
* The {@link TileReaper} associated with this <code>LCTileCache</code>.
*/
private TileReaper m_tileReaper;
}
/* vim:set et sw=4 ts=4: */