/*
* Copyright (C) 2013 Omry Yadan <omry@yadan.net>
* All rights reserved.
*
* See https://github.com/omry/banana/blob/master/BSD-LICENSE for licensing information
*/
package net.yadan.banana.utils;
import net.yadan.banana.list.DoubleLinkedList;
import net.yadan.banana.map.HashMap;
import net.yadan.banana.map.IHashMap;
/**
* LRU (Least Recently Used) cache This cache supports various data types, see
* {@link DataType}
*/
public class LRU {
/**
* Specifies what kind of data this LRU should support:
*/
public enum DataType {
/**
* No data : useful to model LRU behavior with minimal overhead
*/
NONE,
/**
* Data is a primitive long
*/
LONG,
/**
* Data is a primitive int
*/
INT,
/**
* Data is any object. this is the most expenive mode
*/
OBJECT
}
private IHashMap m_lruMap;
private DoubleLinkedList m_lruList;
private Object m_data[];
private DoubleLinkedList m_freeList;
private final int m_maxCapacity;
private static int LIST_NODE_OFFSET = 0;
private static int DATA_OFFSET = 1;
private final DataType m_type;
private int m_mapRecordSize;
private int m_listRecordSize;
private int m_freeListRecordSize;
/**
* Constructs a new LRU which the specific capacity and data type
*
* @param maxCapacity LRU capacity
* @param type data type (see {@link DataType})
*/
public LRU(int maxCapacity, DataType type) {
m_type = type;
m_mapRecordSize = recordSize(type);
m_lruMap = new HashMap(maxCapacity + 1, m_mapRecordSize, 0, 1.0);
m_lruMap.setGrowthFactor(0);
m_maxCapacity = maxCapacity;
m_listRecordSize = 2;
m_lruList = new DoubleLinkedList(maxCapacity + 1, m_listRecordSize, 0);
if (m_type == DataType.OBJECT) {
m_freeListRecordSize = 1;
m_data = new Object[maxCapacity + 1];
m_freeList = new DoubleLinkedList(maxCapacity + 1, m_freeListRecordSize, 1);
for (int i = 0; i < m_data.length; i++) {
int p = m_freeList.appendTail(1);
m_freeList.setInt(p, 0, i);
}
}
}
/**
* Constructs a new LRU which the specific capacity and data type
*
* @param maxCapacity LRU capacity
* @param supportData is true, data type will be {@link DataType#OBJECT}, else
* {@link DataType#NONE}.
*/
public LRU(int maxCapacity, boolean supportData) {
this(maxCapacity, supportData ? DataType.OBJECT : DataType.NONE);
}
/**
* Adds an id without data to the LRU
*
* @param id
*/
public void add(long id) {
add(id, null, null);
}
/**
* Adds an id with Object data to the LRU. LRU must be of type
* {@link DataType#OBJECT}
*
* @param id the id of the item
* @param data object data
*/
public void add(long id, Object data) {
add(id, data, null);
}
/**
* Adds an id with the specified Object data. LRU must be of type
* {@link DataType#OBJECT}
*
* @param id
* @param data
* @param callback callback to notify in case this add triggers an eviction
*/
public void add(long id, Object data, Callback callback) {
if (data != null)
assertDataType(DataType.OBJECT);
add(id, data, 0, 0, callback);
}
/**
* Adds an id with the specified long data. LRU must be of type
* {@link DataType#LONG}
*
* @param id
* @param data
*/
public void addLong(long id, long data) {
addLong(id, data, null);
}
/**
* Adds an id with the specified long data. LRU must be of type
* {@link DataType#LONG}
*
* @param id
* @param data
* @param callback callback to notify in case this add triggers an eviction
*/
public void addLong(long id, long data, Callback callback) {
assertDataType(DataType.LONG);
add(id, null, data, 0, callback);
}
/**
* Adds an id with the specified int data. LRU must be of type
* {@link DataType#INT}
*
* @param id
* @param data
*/
public void addInt(long id, int data) {
addInt(id, data, null);
}
/**
* Adds an id with the specified int data. LRU must be of type
* {@link DataType#INT}
*
* @param id
* @param data
* @param callback callback to notify in case this add triggers an eviction
*/
public void addInt(long id, int data, Callback callback) {
if (m_type != DataType.INT) {
throw new IllegalArgumentException("int data is not supported for this instance");
}
add(id, null, 0, data, callback);
}
private void add(long id, Object data, long longdata, int intdata, Callback callback) {
int node = m_lruMap.findRecord(id);
if (node != -1) {
if (m_type == DataType.OBJECT) {
int index = m_lruMap.getInt(node, DATA_OFFSET);
m_data[index] = data;
}
int item_node = m_lruMap.getInt(node, LIST_NODE_OFFSET);
m_lruList.remove(item_node);
item_node = m_lruList.appendTail(m_listRecordSize);
m_lruList.setLong(item_node, 0, id);
m_lruMap.setInt(node, LIST_NODE_OFFSET, item_node);
// gets optimized away when assertions are off
assert id == m_lruList.getLong(item_node, 0) : "Mismatched value retrieved for " + id;
} else {
// first time we are inserting an item with this key.
int freeIndex = -1;
if (m_type == DataType.OBJECT) {
assert m_freeList.size() > 0 : "Ran out of free items in free pool";
freeIndex = m_freeList.getInt(m_freeList.getHead(), 0);
m_freeList.removeHead();
assert m_data[freeIndex] == null : "Got item with data from free pool";
m_data[freeIndex] = data;
}
int newNode = m_lruMap.createRecord(id, m_mapRecordSize);
int newLRUItem = m_lruList.appendTail(m_listRecordSize);
m_lruList.setLong(newLRUItem, 0, id);
m_lruMap.setInt(newNode, LIST_NODE_OFFSET, newLRUItem);
if (m_type == DataType.OBJECT) {
m_lruMap.setInt(newNode, DATA_OFFSET, freeIndex);
} else if (m_type == DataType.INT) {
m_lruMap.setInt(newNode, DATA_OFFSET, intdata);
} else if (m_type == DataType.LONG) {
m_lruMap.setLong(newNode, DATA_OFFSET, longdata);
}
}
while (m_lruList.size() > m_maxCapacity) {
// remove least recently used
long idd = m_lruList.getLong(m_lruList.getHead(), 0);
m_lruList.removeHead();
int del = m_lruMap.findRecord(idd);
assert del != -1 : "Item " + idd + " not found in map ";
long evictedId = idd;
m_lruMap.remove(idd);
Object evictedData = null;
if (m_type == DataType.OBJECT) {
int dataIndex = m_lruMap.getInt(del, DATA_OFFSET);
evictedData = m_data[dataIndex];
int tail = m_freeList.appendTail(m_freeListRecordSize);
m_freeList.setInt(tail, 0, dataIndex);
m_data[dataIndex] = null;
}
if (callback != null) {
callback.keyEvicted(evictedId, evictedData);
}
}
assert m_lruMap.size() == m_lruList.size() : "Inconsistent map and list sizes";
}
/**
* Sets the value of an id to the specified int. LRU must be of type
* {@link DataType#INT}
*
* @param id the id of the item
* @param value int data
*/
public void setInt(long key, int value) {
assertDataType(DataType.INT);
int n = m_lruMap.findRecord(key);
if (n != -1) {
m_lruMap.setInt(n, DATA_OFFSET, value);
}
}
/**
* Sets the long value for the specified id. LRU must be of type
* {@link DataType#LONG}
*
* @param id the id of the item
* @param value long data
*/
public void setLong(long key, long value) {
assertDataType(DataType.LONG);
int n = m_lruMap.findRecord(key);
if (n != -1) {
m_lruMap.setLong(n, DATA_OFFSET, value);
}
}
/**
* Gets the int associated with the specified id. LRU must be of type
* {@link DataType#INT}
*
* @param id
* @return
* @throws IllegalArgumentException if an item with that id was not found
*/
public int getInt(long id) throws IllegalArgumentException {
assertDataType(DataType.INT);
int node = m_lruMap.findRecord(id);
if (node != -1) {
return m_lruMap.getInt(node, DATA_OFFSET);
} else {
throw new IllegalArgumentException("ID " + id + " not found");
}
}
/**
* Gets the long associated with the specified id. LRU must be of type
* {@link DataType#LONG}
*
* @param id
* @return
* @throws IllegalArgumentException if an item with that id was not found
*/
public long getLong(long id) throws IllegalArgumentException {
assertDataType(DataType.LONG);
int node = m_lruMap.findRecord(id);
if (node != -1) {
return m_lruMap.getLong(node, DATA_OFFSET);
} else {
throw new IllegalArgumentException("ID " + id + " not found");
}
}
/**
* Gets the Object associated with the specified id. LRU must be of type
* {@link DataType#OBJECT}
*
* @param id
* @return the object or null if not found
*/
public Object get(long id) {
assertDataType(DataType.OBJECT);
int node = m_lruMap.findRecord(id);
if (node != -1) {
int index = m_lruMap.getInt(node, DATA_OFFSET);
return m_data[index];
} else
return null;
}
/**
* @return number of items in the LRU
*/
public long size() {
return m_lruMap.size();
}
/**
* @param id
* @return true if the id exists in the LRU
*/
public boolean exists(long id) {
return m_lruMap.containsKey(id);
}
/**
* Computes the approximate size in bytes an LRU with the specified size and
* type will consume
*
* @param maxCapacity capacity
* @param type type
* @return size in bytes
*/
public static long computerMemoryUsage(int maxCapacity, DataType type) {
long ret = 4 * HashMap.getIntArraySize(maxCapacity + 1, recordSize(type));
if (type == DataType.OBJECT) {
ret += 8 * (maxCapacity + 1); // estimate for object pool array
ret += 4 * DoubleLinkedList.getIntArraySize(maxCapacity + 1, 1); // for
// freelist
}
return ret;
}
@Override
public String toString() {
String data = "";
switch (m_type) {
case NONE:
break;
case OBJECT:
data = " with Object data support";
break;
case LONG:
data = " with long data support";
break;
case INT:
data = " with int data support";
break;
}
return "LRU : " + size() + "/" + m_maxCapacity + data;
}
public int capacity() {
return m_maxCapacity;
}
public interface Callback {
public void keyEvicted(long key, Object data);
}
private static int recordSize(DataType type) {
int recordSize = 1;
int dataSize = 0;
switch (type) {
case NONE:
dataSize = 0;
break;
case LONG:
dataSize = 2;
break;
case INT:
case OBJECT:
dataSize = 1;
break;
}
recordSize += dataSize;
return recordSize;
}
private void assertDataType(DataType type) {
if (m_type != type)
throw new UnsupportedOperationException("Data type is not " + type);
}
}