package net.i2p.data; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Map; import net.i2p.I2PAppContext; import net.i2p.util.LHMCache; import net.i2p.util.SimpleByteCache; import net.i2p.util.SystemVersion; /** * A least recently used cache with a max size, for SimpleDataStructures. * The index to the cache is the first 4 bytes of the data, so * the data must be sufficiently random. * * This caches the SDS objects, and also uses SimpleByteCache to cache * the unused byte arrays themselves * * Following is sample usage: * <pre> private static final SDSCache<Foo> _cache = new SDSCache(Foo.class, LENGTH, 1024); public static Foo create(byte[] data) { return _cache.get(data); } public static Foo create(byte[] data, int off) { return _cache.get(data, off); } public static Foo create(InputStream in) throws IOException { return _cache.get(in); } * </pre> * @since 0.8.3 * @author zzz */ public class SDSCache<V extends SimpleDataStructure> { //private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(SDSCache.class); private static final double MIN_FACTOR = 0.20; private static final double MAX_FACTOR = 5.0; private static final double FACTOR; static { long maxMemory = SystemVersion.getMaxMemory(); FACTOR = Math.max(MIN_FACTOR, Math.min(MAX_FACTOR, maxMemory / (128*1024*1024d))); } /** the LRU cache */ private final Map<Integer, WeakReference<V>> _cache; /** the byte array length for the class we are caching */ private final int _datalen; /** the constructor for the class we are caching */ private final Constructor<V> _rvCon; private final String _statName; /** * @param rvClass the class that we are storing, i.e. an extension of SimpleDataStructure * @param len the length of the byte array in the SimpleDataStructure * @param max maximum size of the cache assuming 128MB of mem. * The actual max size will be scaled based on available memory. */ public SDSCache(Class<V> rvClass, int len, int max) { int size = (int) (max * FACTOR); _cache = new LHMCache<Integer, WeakReference<V>>(size); _datalen = len; try { _rvCon = rvClass.getConstructor(byte[].class); } catch (NoSuchMethodException e) { throw new RuntimeException("SDSCache init error", e); } _statName = "SDSCache." + rvClass.getSimpleName(); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("New SDSCache for " + rvClass + " data size: " + len + // " max: " + size + " max mem: " + (len * size)); I2PAppContext.getGlobalContext().statManager().createRateStat(_statName, "Hit rate", "Router", new long[] { 10*60*1000 }); I2PAppContext.getGlobalContext().addShutdownTask(new Shutdown()); } /** * @since 0.8.8 */ private class Shutdown implements Runnable { public void run() { clear(); } } /** * @since 0.9.17 */ public void clear() { synchronized(_cache) { _cache.clear(); } } /** * WARNING - If the SDS is found in the cache, the passed-in * byte array will be returned to the SimpleByteCache for reuse. * Do NOT save a reference to the passed-in data, or use or modify it, * after this call. * * @param data non-null, the byte array for the SimpleDataStructure * @return the cached value if available, otherwise * makes a new object and returns it * @throws IllegalArgumentException if data is not the correct number of bytes * @throws NullPointerException */ public V get(byte[] data) { if (data == null) throw new NullPointerException("Don't pull null data from the cache"); int found; V rv; Integer key = hashCodeOf(data); synchronized(_cache) { WeakReference<V> ref = _cache.get(key); if (ref != null) rv = ref.get(); else rv = null; if (rv != null && Arrays.equals(data, rv.getData())) { // found it, we don't need the data passed in any more SimpleByteCache.release(data); found = 1; } else { // make a new one try { rv = _rvCon.newInstance(new Object[] { data } ); } catch (InstantiationException e) { throw new RuntimeException("SDSCache error", e); } catch (IllegalAccessException e) { throw new RuntimeException("SDSCache error", e); } catch (InvocationTargetException e) { throw new RuntimeException("SDSCache error", e); } _cache.put(key, new WeakReference<V>(rv)); found = 0; } } I2PAppContext.getGlobalContext().statManager().addRateData(_statName, found); return rv; } /* * @param b non-null byte array containing the data, data will be copied to not hold the reference * @param off offset in the array to start reading from * @return the cached value if available, otherwise * makes a new object and returns it * @throws ArrayIndexOutOfBoundsException if not enough bytes * @throws NullPointerException */ public V get(byte[] b, int off) { byte[] data = SimpleByteCache.acquire(_datalen); System.arraycopy(b, off, data, 0, _datalen); return get(data); } /* * @param in a stream from which the bytes will be read * @return the cached value if available, otherwise * makes a new object and returns it * @throws IOException if not enough bytes */ public V get(InputStream in) throws IOException { byte[] data = SimpleByteCache.acquire(_datalen); int read = DataHelper.read(in, data); if (read != _datalen) throw new EOFException("Not enough bytes to read the data"); return get(data); } /** * We assume the data has enough randomness in it, so use the first 4 bytes for speed. */ private static Integer hashCodeOf(byte[] data) { int rv = data[0]; for (int i = 1; i < 4; i++) rv ^= (data[i] << (i*8)); return Integer.valueOf(rv); } }