/* JAI-Ext - OpenSource Java Advanced Image Extensions Library
* http://www.geo-solutions.it/
* Copyright 2014 GeoSolutions
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.geosolutions.concurrent;
import it.geosolutions.concurrent.ConcurrentTileCache.Actions;
import java.awt.Point;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.TileCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.cache.Weigher;
import com.sun.media.jai.util.CacheDiagnostics;
/**
* This implementation of the TileCache class uses a Guava Cache and a multimap in order to provide a better concurrency handling. The first object
* contains all the cached tiles while the second one contains the mapping of the tile keys for each image. This class implements
* {@link CacheDiagnostics} in order to get the statistics associated to the {@link TileCache}. The user can define the cache memory capacity, the
* concurrency level (which indicates in how many segments the cache must be divided), the threshold of the total memory to use and a boolean
* indicating if the diagnostic must be enabled.
*
* @author Nicola Lagomarsini GeoSolutions S.A.S.
*
*/
public class ConcurrentTileCacheMultiMap extends Observable implements TileCache, CacheDiagnostics {
/** The default memory threshold of the cache. */
public static final float DEFAULT_MEMORY_THRESHOLD = 0.75F;
/** The default memory capacity of the cache (16 MB). */
public static final long DEFAULT_MEMORY_CACHE = 16L * 1024L * 1024L;
/** The default diagnostic settings */
public static final boolean DEFAULT_DIAGNOSTIC = false;
/** The default concurrency settings */
public static final int DEFAULT_CONCURRENCY_LEVEL = 4;
/**
* The tile cache. A Guava Cache is used to cache the tiles. The "key" is a <code>Object</code>. The "value" is a CachedTileImpl.
*/
private Cache<Object, CachedTileImpl> cacheObject;
/**
* A concurrent multimap used for mapping the tile keys for each image
*/
private ConcurrentHashMap<Object, Set<Object>> multimap;
/** The memory capacity of the cache. */
private long memoryCacheCapacity;
/** The concurrency level of the cache. */
private int concurrencyLevel;
/** The amount of memory to keep after memory control */
private float memoryCacheThreshold = DEFAULT_MEMORY_THRESHOLD;
/** diagnosticEnabled enable/disable */
private volatile boolean diagnosticEnabled = DEFAULT_DIAGNOSTIC;
/**
* Logger to use for reporting the informations about the TileCache operations.
*/
private final static Logger LOGGER = Logger.getLogger(ConcurrentTileCacheMultiMap.class
.toString());
/**
* Memory overhead in bytes for tracking the tiles used by each image
*/
private static final long TILE_TRACKING_OVERHEAD = 50;
public ConcurrentTileCacheMultiMap() {
this(DEFAULT_MEMORY_CACHE, DEFAULT_DIAGNOSTIC, DEFAULT_MEMORY_THRESHOLD,
DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentTileCacheMultiMap(long memoryCacheCapacity, boolean diagnostic,
float mem_threshold, int concurrencyLevel) {
if (memoryCacheCapacity < 0) {
throw new IllegalArgumentException("Memory capacity too small");
}
this.memoryCacheThreshold = mem_threshold;
this.diagnosticEnabled = diagnostic;
this.memoryCacheCapacity = memoryCacheCapacity;
this.concurrencyLevel = concurrencyLevel;
// cache creation
cacheObject = buildCache();
// multimap creation
multimap = new ConcurrentHashMap<Object, Set<Object>>();
}
/** Add a new tile to the cache */
public void add(RenderedImage owner, int tileX, int tileY, Raster data) {
add(owner, tileX, tileY, data, null);
}
/** Add a new tile to the cache */
public void add(RenderedImage owner, int tileX, int tileY, Raster data, Object tileCacheMetric) {
// when computation fails this method is called with a null raster,
// avoid logging an extra NPE
if(data == null) {
return;
}
// This tile is not in the cache; create a new CachedTileImpl.
// else just update.
// Key associated to the image
Object imageKey = CachedTileImpl.hashKey(owner);
// old tile
CachedTileImpl cti;
// create a new tile
CachedTileImpl cti_new = new CachedTileImpl(owner, tileX, tileY, data, tileCacheMetric);
if (diagnosticEnabled) {
// if the tile is already cached
cti = (CachedTileImpl) cacheObject.asMap().putIfAbsent(cti_new.key, cti_new);
synchronized (cacheObject) {
if (cti != null) {
cti.updateTileTimeStamp();
cti.setAction(Actions.SUBSTITUTION_FROM_ADD);
setChanged();
notifyObservers(cti);
}
// Update the tile action in order to notify it to the observers
cti_new.setAction(Actions.ADDITION);
setChanged();
notifyObservers(cti_new);
updateMultiMap(cti_new.key, imageKey);
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Added new Tile Image key " + imageKey);
}
// new tile insertion
cacheObject.asMap().putIfAbsent(cti_new.key, cti_new);
// Atomically adds a new Map if needed and then adds a new tile inside the MultiMap.
updateMultiMap(cti_new.key, imageKey);
}
}
private long getTileSize(CachedTileImpl cti) {
return cti.getTileSize() + TILE_TRACKING_OVERHEAD;
}
/** Removes the selected tile from the cache */
public void remove(RenderedImage owner, int tileX, int tileY) {
// Calculation of the tile key
Object key = CachedTileImpl.hashKey(owner, tileX, tileY);
// remove operation
removeTileByKey(key);
}
/** Retrieves the selected tile from the cache */
public Raster getTile(RenderedImage owner, int tileX, int tileY) {
// Calculation of the tile key
Object key = CachedTileImpl.hashKey(owner, tileX, tileY);
// Get operation
return getTileFromKey(key);
}
/**
* Retrieves an array of all tiles in the cache which are owned by the image. May be <code>null</code> if there were no tiles in the cache. The
* array contains no null entries.
*/
public Raster[] getTiles(RenderedImage owner) {
// instantiation of the result array
Raster[] tilesData = null;
// Calculation of the key associated to the image
Object imageKey = CachedTileImpl.hashKey(owner);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Getting image Tiles Image key " + imageKey);
}
// Selection of the tile keys for the image
Set<Object> keys = multimap.get(imageKey);
// If no key is found then a null object is returned
if (keys == null || keys.isEmpty()) {
return tilesData;
}
// Else it is created an iterator on the tile keys
Iterator<Object> it = keys.iterator();
// Another check on the iterator
if (it.hasNext()) {
// arbitrarily set a temporary vector size
Vector<Raster> tempData = new Vector<Raster>(10, 20);
// cycle through all the tile keys present in the multimap and check if they are in the
// cache...
while (it.hasNext()) {
Object key = it.next();
// get the tile from the key
Raster rasterTile = getTileFromKey(key);
// ...then add to the vector if present
if (rasterTile != null) {
tempData.add(rasterTile);
}
}
// Vector size
int tmpsize = tempData.size();
if (tmpsize > 0) {
tilesData = (Raster[]) tempData.toArray(new Raster[tmpsize]);
}
}
return tilesData;
}
/**
* Removes all tiles in the cache which are owned by the image.
*/
public void removeTiles(RenderedImage owner) {
// Calculation of the key associated to the image
Object imageKey = CachedTileImpl.hashKey(owner);
if (diagnosticEnabled) {
// Selection of the keys associated to the image and removal of each of them
Set<Object> keys = multimap.remove(imageKey);
if (keys != null) {
Iterator<Object> it = keys.iterator();
while (it.hasNext()) {
Object key = it.next();
removeTileByKey(key);
}
}
} else {
// Get the keys associated to the image and remove them
Set<Object> keys = multimap.remove(imageKey);
if (keys != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing image Tiles Image key " + imageKey);
}
cacheObject.invalidateAll(keys);
}
}
}
/**
* Adds all tiles in the Point array which are owned by the image.
*/
public void addTiles(RenderedImage owner, Point[] tileIndices, Raster[] tiles,
Object tileCacheMetric) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Addeding Tiles");
}
// cycle through the array for adding tiles
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);
}
}
/**
* Retrieves an array of tiles in the cache which are specified by the Point array and owned by the image. May be <code>null</code> if there were
* not in the cache. The array contains null entries.
*/
public Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
// instantiation of the array
Raster[] tilesData = new Raster[tileIndices.length];
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Getting Tiles at the selected positions");
}
// cycle through the array for getting tiles
for (int i = 0; i < tilesData.length; i++) {
int tileX = tileIndices[i].x;
int tileY = tileIndices[i].y;
Raster rasterData = getTile(owner, tileX, tileY);
// even if the tile is not present it is inserted in the array
if (rasterData == null) {
tilesData[i] = null;
} else {
// found tile in cache
tilesData[i] = rasterData;
}
}
return tilesData;
}
/** Removes all tiles present in the cache without checking for the image owner */
public void flush() {
synchronized (cacheObject) {
// It is necessary to clear all the elements
// from the old cache.
if (diagnosticEnabled) {
// Creation of an iterator for accessing to every tile in the cache
Iterator<Object> keys = cacheObject.asMap().keySet().iterator();
// cycle across the cache for removing and updating every tile
while (keys.hasNext()) {
Object key = keys.next();
CachedTileImpl cti = (CachedTileImpl) cacheObject.asMap().remove(key);
// diagnosticEnabled
cti.setAction(Actions.REMOVAL_FROM_FLUSH);
setChanged();
notifyObservers(cti);
}
} else {
// Invalidation of all the keys of the cache
cacheObject.invalidateAll();
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Flushing cache");
}
// Cache creation
cacheObject = buildCache();
// multimap creation
multimap = new ConcurrentHashMap<Object, Set<Object>>();
}
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public void memoryControl() {
throw new UnsupportedOperationException("Memory Control not supported");
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public void setTileCapacity(int tileCapacity) {
throw new UnsupportedOperationException("Deprecated Operation");
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public int getTileCapacity() {
throw new UnsupportedOperationException("Deprecated Operation");
}
/** Sets the cache memory capacity and then flush and rebuild the cache */
public void setMemoryCapacity(long memoryCacheCapacity) {
synchronized (cacheObject) {
if (memoryCacheCapacity < 0) {
throw new IllegalArgumentException("Memory capacity too small");
} else {
this.memoryCacheCapacity = memoryCacheCapacity;
// The flush is done in order to rebuild the cache with the new settings
flush();
}
}
}
/** Retrieve the cache memory capacity */
public long getMemoryCapacity() {
return memoryCacheCapacity;
}
/** Sets the cache memory threshold and then flush and rebuild the cache */
public void setMemoryThreshold(float mt) {
synchronized (cacheObject) {
if (mt < 0.0F || mt > 1.0F) {
throw new IllegalArgumentException("Memory threshold should be between 0 and 1");
} else {
memoryCacheThreshold = mt;
// The flush is done in order to rebuild the cache with the new settings
flush();
}
}
}
/** Retrieve the cache memory threshold */
public float getMemoryThreshold() {
return memoryCacheThreshold;
}
/** Sets the cache ConcurrencyLevel and then flush and rebuild the cache */
public void setConcurrencyLevel(int concurrency) {
synchronized (cacheObject) {
if (concurrency < 1) {
throw new IllegalArgumentException("ConcurrencyLevel must be at least 1");
} else {
concurrencyLevel = concurrency;
// The flush is done in order to rebuild the cache with the new settings
flush();
}
}
}
/** Retrieve the cache concurrency level */
public int getConcurrencyLevel() {
return concurrencyLevel;
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public void setTileComparator(Comparator comparator) {
throw new UnsupportedOperationException("Comparator not supported");
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public Comparator getTileComparator() {
throw new UnsupportedOperationException("Comparator not supported");
}
/** Disables diagnosticEnabled for the observers */
public void disableDiagnostics() {
synchronized (cacheObject) {
diagnosticEnabled = false;
// The flush is done in order to rebuild the cache with the new settings
flush();
}
}
/** Enables diagnosticEnabled for the observers */
public void enableDiagnostics() {
synchronized (cacheObject) {
diagnosticEnabled = true;
// The flush is done in order to rebuild the cache with the new settings
flush();
}
}
/** Retrieves the hit count from the cache statistics */
public long getCacheHitCount() {
if (diagnosticEnabled) {
return cacheObject.stats().hitCount();
}
return 0;
}
/** Retrieves the current memory size of the cache */
public long getCacheMemoryUsed() {
Iterator<Object> keys = cacheObject.asMap().keySet().iterator();
long memoryUsed = 0;
while (keys.hasNext()) {
Object key = keys.next();
CachedTileImpl cti = (CachedTileImpl) cacheObject.getIfPresent(key);
memoryUsed += getTileSize(cti);
}
return memoryUsed;
}
/** Retrieves the miss count from the cache statistics */
public long getCacheMissCount() {
if (diagnosticEnabled) {
return cacheObject.stats().missCount();
}
return 0;
}
/** Retrieves the number of tiles in the cache */
public long getCacheTileCount() {
return cacheObject.size();
}
/**
* Not Supported
*
* @throws UnsupportedOperationException
*/
public void resetCounts() {
throw new UnsupportedOperationException("Operation not supported");
}
/**
* Creation of a listener to use for handling the removed tiles
*
* @param diagnostic
* @return
*/
private RemovalListener<Object, CachedTileImpl> createListener(final boolean diagnostic) {
return new RemovalListener<Object, CachedTileImpl>() {
public void onRemoval(RemovalNotification<Object, CachedTileImpl> n) {
// if a tile is manually removed, the diagnosticEnabled already consider
// it in
// the remove() method
if (diagnostic) {
synchronized (cacheObject) {
CachedTileImpl cti = n.getValue();
// Update of the tile action
if (n.wasEvicted()) {
cti.setAction(Actions.REMOVAL_FROM_EVICTION);
} else {
cti.setAction(Actions.MANUAL_REMOVAL);
}
// Removal from the multimap
removeTileFromMultiMap(cti);
setChanged();
notifyObservers(cti);
}
} else {
CachedTileImpl cti = n.getValue();
if (n.getCause() == RemovalCause.SIZE) {
// Logging if the tile is removed because the size is exceeded
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing from MultiMap for size");
}
}
removeTileFromMultiMap(cti);
}
}
};
}
/**
* Method for removing the tile keys from the multimap. If the KeySet associated to the image is empty, it is removed from the multimap.
*
* @param cti
*/
private void removeTileFromMultiMap(CachedTileImpl cti) {
// Tile key
Object key = cti.getKey();
// Image key
Object imageKey = cti.getImageKey();
// KeySet associated to the image
Set<Object> tileKeys = multimap.get(imageKey);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing tile from MultiMap Image key " + imageKey);
}
if (tileKeys != null) {
// Removal of the keys
tileKeys.remove(key);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removed Tile Image key " + imageKey);
}
// If the KeySet is empty then it is removed from the multimap
if (tileKeys.isEmpty()) {
multimap.remove(imageKey);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removed image SET Image key " + imageKey);
}
}
}
}
/** Private cache creation method */
private Cache<Object, CachedTileImpl> buildCache() {
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
builder.maximumWeight((long) (memoryCacheCapacity * memoryCacheThreshold))
.concurrencyLevel(concurrencyLevel).weigher(new Weigher<Object, CachedTileImpl>() {
public int weigh(Object o, CachedTileImpl cti) {
return (int) getTileSize(cti);
}
});
// Setting of the listener
builder.removalListener(createListener(diagnosticEnabled));
// Enable statistics only when the diagnostic flag is set to true;
if(diagnosticEnabled){
builder.recordStats();
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Building Cache");
}
return builder.build();
}
/**
* Update of the multimap when a tile is added.
*
* @param key
* @param imageKey
*/
private void updateMultiMap(Object key, Object imageKey) {
Set<Object> tileKeys = null;
// Check if the multimap contains the keys for the image
tileKeys = multimap.get(imageKey);
if (tileKeys == null) {
// If no key is present then a new KeySet is created and then added to the multimap
tileKeys = new ConcurrentSkipListSet<Object>();
Set<Object> previousTileKeys = multimap.putIfAbsent(imageKey, tileKeys);
if(previousTileKeys != null) {
tileKeys = previousTileKeys;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Created new Set for the image Image key " + imageKey);
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Added Tile to the set Image key " + imageKey);
}
// Finally the tile key is added.
tileKeys.add(key);
}
/**
* Removes the tile associated to the key.
*
* @param key
*/
private void removeTileByKey(Object key) {
// check if the tile is still in cache
CachedTileImpl cti = (CachedTileImpl) cacheObject.getIfPresent(key);
// if so the tile is deleted (even if another thread write on it)
if (cti != null) {
if (diagnosticEnabled) {
synchronized (cacheObject) {
// Upgrade the tile action
cti.setAction(Actions.ABOUT_TO_REMOVAL);
setChanged();
notifyObservers(cti);
// Removal of the tile
cti = (CachedTileImpl) cacheObject.asMap().remove(key);
if (cti != null) {
// Upgrade the tile action
cti.setAction(Actions.MANUAL_REMOVAL);
setChanged();
notifyObservers(cti);
}
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removed Tile Image key " + cti.getImageKey());
}
// Discard the tile from the cache
cacheObject.invalidate(key);
}
}
}
/**
* Gets the tile associated to the key.
*
* @param key
* @return
*/
private Raster getTileFromKey(Object key) {
Raster tileData = null;
// check if the tile is present
CachedTileImpl cti = (CachedTileImpl) cacheObject.asMap().get(key);
// If not tile is found, null is returned
if (cti == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Null Tile returned");
}
return null;
}
if (diagnosticEnabled) {
synchronized (cacheObject) {
// Update last-access time for diagnosticEnabled
cti.updateTileTimeStamp();
cti.setAction(Actions.UPDATING_TILE_FROM_GETTILE);
setChanged();
notifyObservers(cti);
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Get the selected tile Image key " + cti.getImageKey());
}
// return the selected tile
tileData = cti.getTile();
return tileData;
}
}