/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.jai; import java.awt.Point; import java.awt.image.ComponentSampleModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.awt.image.SinglePixelPackedSampleModel; import java.awt.image.WritableRaster; import java.lang.ref.SoftReference; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.TileFactory; import javax.media.jai.TileRecycler; import org.apache.commons.beanutils.PropertyUtils; import org.geotools.util.logging.Logging; import com.sun.media.jai.util.DataBufferUtils; /** * A thread safe recycling tile factory that using Java 5 Concurrent data structures * * @author Andrea Aime - GeoSolutions */ public class ConcurrentTileFactory implements TileFactory, TileRecycler { /** * Cache of the tile recycled arrays */ private volatile ArrayCache recycledArrays = new ArrayCache(); /** * A concurrent multimap geared towards tile data array caching * * @author Andrea Aime - GeoSolutions * */ private static class ArrayCache extends ConcurrentHashMap<Long, ConcurrentLinkedQueue<SoftReference<?>>> { private static final long serialVersionUID = -6905685668738379653L; static final Logger LOGGER = Logging.getLogger(ConcurrentTileFactory.class); /** * Retrieve an array of the specified type and length. */ Object getRecycledArray(int arrayType, long numBanks, long arrayLength) { Long key = getKey(arrayType, numBanks, arrayLength); ConcurrentLinkedQueue<SoftReference<?>> arrays = get(key); if (arrays != null) { SoftReference<?> arrayRef; while ((arrayRef = arrays.poll()) != null) { Object array = arrayRef.get(); if (array != null) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Recycling tile hit on type:{1}, banks: {2}, arrayLength: {3}", new Object[] { arrayType, numBanks, arrayLength }); } return array; } } } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Recycling tile miss on type:{1}, banks: {2}, arrayLength: {3}", new Object[] { arrayType, numBanks, arrayLength }); } return null; } private Long getKey(int arrayType, long numBanks, long arrayLength) { return Long.valueOf((((long) arrayType << 56) | numBanks << 32 | arrayLength)); } public void recycleTile(Raster tile) { DataBuffer db = tile.getDataBuffer(); Long key = getKey(db.getDataType(), db.getNumBanks(), db.getSize()); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Recycling tile hit on type:{1}, banks: {2}, arrayLength: {3}", new Object[] { db.getDataType(), db.getNumBanks(), db.getSize() }); } ConcurrentLinkedQueue<SoftReference<?>> arrays = get(key); if (arrays == null) { arrays = new ConcurrentLinkedQueue<SoftReference<?>>(); arrays.add(getBankReference(db)); put(key, arrays); return; } else { arrays.add(getBankReference(db)); } } /** * Returns a <code>SoftReference</code> to the actual data stored into the DataBuffer */ private static SoftReference<?> getBankReference(DataBuffer db) { try { Object array = PropertyUtils.getProperty(db, "bankData"); return new SoftReference<Object>(array); } catch (Exception e) { throw new UnsupportedOperationException("Unknown data buffer type " + db); } } } /** * Constructs a <code>RecyclingTileFactory</code>. */ public ConcurrentTileFactory() { } /** * Returns <code>true</code>. */ public boolean canReclaimMemory() { return true; } /** * Returns <code>true</code>. */ public boolean isMemoryCache() { return true; } /** * Always returns -1, does not do used memory accounting */ public long getMemoryUsed() { return -1; } /** * Clean up the cache */ public void flush() { recycledArrays.clear(); } /** * Builds a new tile, eventually recycling the data array backing it */ public WritableRaster createTile(SampleModel sampleModel, Point location) { // sanity checks if (sampleModel == null) { throw new NullPointerException("sampleModel cannot be null"); } if (location == null) { location = new Point(0, 0); } DataBuffer db = null; // get the three elements making the key into the recycled array map int type = sampleModel.getTransferType(); long numBanks = 0; long size = 0; if (sampleModel instanceof ComponentSampleModel) { ComponentSampleModel csm = (ComponentSampleModel) sampleModel; numBanks = getNumBanksCSM(csm); size = getBufferSizeCSM(csm); } else if (sampleModel instanceof MultiPixelPackedSampleModel) { MultiPixelPackedSampleModel mppsm = (MultiPixelPackedSampleModel) sampleModel; numBanks = 1; int dataTypeSize = DataBuffer.getDataTypeSize(type); size = mppsm.getScanlineStride() * mppsm.getHeight() + (mppsm.getDataBitOffset() + dataTypeSize - 1) / dataTypeSize; } else if (sampleModel instanceof SinglePixelPackedSampleModel) { SinglePixelPackedSampleModel sppsm = (SinglePixelPackedSampleModel) sampleModel; numBanks = 1; size = sppsm.getScanlineStride() * (sppsm.getHeight() - 1) + sppsm.getWidth(); } if (size > 0) { // try to build a new data buffer starting from Object array = recycledArrays.getRecycledArray(type, numBanks, size); if (array != null) { switch (type) { case DataBuffer.TYPE_BYTE: { byte[][] bankData = (byte[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], (byte) 0); } db = new DataBufferByte(bankData, (int) size); } break; case DataBuffer.TYPE_USHORT: { short[][] bankData = (short[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], (short) 0); } db = new DataBufferUShort(bankData, (int) size); } break; case DataBuffer.TYPE_SHORT: { short[][] bankData = (short[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], (short) 0); } db = new DataBufferShort(bankData, (int) size); } break; case DataBuffer.TYPE_INT: { int[][] bankData = (int[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], 0); } db = new DataBufferInt(bankData, (int) size); } break; case DataBuffer.TYPE_FLOAT: { float[][] bankData = (float[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], 0.0F); } db = DataBufferUtils.createDataBufferFloat(bankData, (int) size); } break; case DataBuffer.TYPE_DOUBLE: { double[][] bankData = (double[][]) array; for (int i = 0; i < numBanks; i++) { Arrays.fill(bankData[i], 0.0); } db = DataBufferUtils.createDataBufferDouble(bankData, (int) size); } break; default: throw new IllegalArgumentException("Unknown array type"); } } } if (db == null) { db = sampleModel.createDataBuffer(); } return Raster.createWritableRaster(sampleModel, db, location); } static long getBufferSizeCSM(ComponentSampleModel csm) { int[] bandOffsets = csm.getBandOffsets(); int maxBandOff = bandOffsets[0]; for (int i = 1; i < bandOffsets.length; i++) maxBandOff = Math.max(maxBandOff, bandOffsets[i]); long size = 0; if (maxBandOff >= 0) size += maxBandOff + 1; int pixelStride = csm.getPixelStride(); if (pixelStride > 0) size += pixelStride * (csm.getWidth() - 1); int scanlineStride = csm.getScanlineStride(); if (scanlineStride > 0) size += scanlineStride * (csm.getHeight() - 1); return size; } private static long getNumBanksCSM(ComponentSampleModel csm) { int[] bankIndices = csm.getBankIndices(); int maxIndex = bankIndices[0]; for (int i = 1; i < bankIndices.length; i++) { int bankIndex = bankIndices[i]; if (bankIndex > maxIndex) { maxIndex = bankIndex; } } return maxIndex + 1; } /** * Recycle the given tile. */ public void recycleTile(Raster tile) { if(tile.getWidth() != tile.getHeight() || tile.getWidth() > 512) { return; } recycledArrays.recycleTile(tile); } }