package visad.data;
import java.util.logging.Logger;
import visad.VisADException;
/**
* Memory cache for <code>FlatField</code>s. Maintains a fixed number of cache
* arrays in memory. When a cache member is requested which is not currently
* available in the cache it is loaded directly into one of the cache arrays.
* This prevents the allocation or garbage collection of data arrays when they
* are created. This allows caching to happen close to the data, rather than the
* rendering and is intended to allow for large animation loops with as little
* an affect as possible on rendering. This can however lead to large latency at
* startup due to reading data sources and copying them to the cache arrays.
* <p>
* Most of this class was modeled after the <code>FileFlatField</code>. This cache,
* however, is not a static cache, it is a instance cache. The affect of this is
* that if you create several large caches you can very quickly run out of memory.
*/
public class FlatFieldCache {
private static Logger log = Logger.getLogger(FlatFieldCache.class.getName());
static interface CacheOwner {
public String getId();
};
/**
* Simple cache strategy based on the time a <code>FlatField</code> was last accessed.
*/
static class AccessTimeCacheStrategy implements FlatFieldCacheStrategy {
public int allocate(Entry[] entries) {
if (entries == null || entries.length == 0) return -1;
int availableIdx = 0;
long oldest = entries[0] != null ? entries[0].lastAccessed : 0;
for (int ii = 0; ii < entries.length; ii++) {
if (entries[ii] == null) {
availableIdx = ii;
return availableIdx;
} else if (entries[ii].lastAccessed < oldest) {
oldest = entries[ii].lastAccessed;
availableIdx = ii;
}
}
return availableIdx;
}
}
/**
* Cache entry metadata.
*/
static class Entry {
CacheOwner owner;
float[][] data;
boolean dirty = false;
long lastAccessed;
long size;
Entry(CacheOwner owner, float[][] data) {
this.data = data;
this.owner = owner;
lastAccessed = System.currentTimeMillis();
}
public String toString() {
return "<Entry lastAccessed="+lastAccessed+" dirty="+dirty+" owner="+owner.getId()+">";
}
}
private final Entry[] cache;
private final int cacheSize;
private FlatFieldCacheStrategy strategy;
/**
* Create a cache with a fixed size and the default strategy.
* @param cacheSize
*/
public FlatFieldCache(int cacheSize) {
this(cacheSize, new AccessTimeCacheStrategy());
}
/**
* Initialize cache.
* @param cacheSize Number of <code>FlatFields</code> to maintain in cache, >= 1.
* @param strategy How cache allocation is performed.
*/
public FlatFieldCache(int cacheSize, FlatFieldCacheStrategy strategy) {
if (cacheSize < 1) throw new IllegalArgumentException("cache size must be >= 1");
this.cacheSize = cacheSize;
this.strategy = strategy;
cache = new Entry[cacheSize];
}
protected void updateEntry(Entry entry, float[][] data, CacheOwner owner) {
for (int ii = 0; ii < data.length; ii++) {
System.arraycopy(data[ii], 0, entry.data[ii], 0, data[ii].length);
}
entry.owner = owner;
entry.dirty = false;
entry.lastAccessed = System.currentTimeMillis();
}
/**
* Does the work of getting data from the cache.
*
* @param owner
* @param fileAccessor
* @return cache data array
*/
protected synchronized float[][] getData(AreaImageCacheAdapter owner, FlatFieldCacheAccessor fileAccessor) {
// if owner array is null,
// assume this object got serialized & unserialized
if (cache == null) {
log.fine("Cache was null, returning");
return null;
}
for (int ii = 0; ii < cache.length; ii++) {
// if the owner is the same as the entries owner
// we have the right entry
if (cache[ii] != null && owner == cache[ii].owner) {
cache[ii].lastAccessed = System.currentTimeMillis();
return cache[ii].data;
}
}
// this FileFlatField does not own a cache entry, so invoke
// CahceStrategy.allocate to allocate one, possibly by taking
// one, possibly by taking one from another FileFlatField;
// this will be an area for lots of thought and experimentation;
float[][] range = null;
int idx = strategy.allocate(cache);
// cannot allocate
if (idx == -1) {
range = fileAccessor.readFlatFieldFloats();
} else {
// entry should only be null once, whence we should create a new one
if (cache[idx] == null) {
cache[idx] = new Entry(owner, fileAccessor.readFlatFieldFloats());
} else {
if (cache[idx].dirty) {
try {
flushCache(cache[idx], fileAccessor);
} catch (VisADException e) {
throw new FlatFieldCacheError("Could not flush to cache", e);
}
}
// update the cached entry with the new values
try {
float[][] data = fileAccessor.readFlatFieldFloats();
if (cache[idx].data == null) {
cache[idx].data = data;
} else {
updateEntry(cache[idx], data, owner);
}
} catch (Exception e) {
throw new FlatFieldCacheError("Could not update cache entry", e);
}
}
range = cache[idx] != null ? cache[idx].data : null;
}
return range;
}
/**
* Not currently implemented.
* @param entry
* @param fileAccessor
* @throws UnsupportedOperationException All the time.
*/
public void flushCache(Entry entry, FlatFieldCacheAccessor fileAccessor) throws VisADException {
throw new UnsupportedOperationException("FlatFieldCache.flushCache not implemented");
}
/**
* Don't do this.
*
* @param owner The owner of the cache entry to mark as dirty.
* @param dirty
*/
public void setDirty(AreaImageCacheAdapter owner, boolean dirty) {
for (int ii = 0; ii < cache.length; ii++) {
if (cache[ii].owner == owner) {
cache[ii].dirty = dirty;
}
}
}
public String toString() {
StringBuffer buf = new StringBuffer("<FlatFieldCache size=" + this.cacheSize + "\n");
for (Entry entry : cache) {
buf.append("\t" + (entry == null ? null : entry.toString()) + "\n");
}
buf.append(">");
return buf.toString();
}
public int getSize() {
return cacheSize;
}
}