/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.cache;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import com.opendoorlogistics.api.cache.ObjectCache;
import com.opendoorlogistics.core.utils.Pair;
/**
* Cache which stores only the recently used objects. Objects are stored as soft references
* and can hence still get dropped from the cache early if the memory is really needed.
* @author Phil
*
*/
final public class RecentlyUsedCache implements ObjectCache{
private long timeIndex=0;
private long totalBytes;
private final long bytesLimit;
private final long entriesLimit;
private WeakHashMap<Object, CacheEntry> cached = new WeakHashMap<>();
private final String name;
private boolean logToConsole=false;
public RecentlyUsedCache(String name,long bytesLimit){
this(name, bytesLimit, Long.MAX_VALUE);
}
public RecentlyUsedCache(String name,long bytesLimit, long entriesLimit){
this.name = name;
this.bytesLimit = bytesLimit;
this.entriesLimit = entriesLimit;
}
private static class CacheEntry implements Comparable<CacheEntry>{
static final int CONTAINER_OVERHEAD_BYTES = 8 + 16 + 4 + 8 + 8; // rough guess....
final Object key;
final SoftReference<Object> data;
final long nbBytes;
long lastUsed;
CacheEntry(Object key,Object obj, long nbBytes) {
this.key = key;
this.data = new SoftReference<Object>(obj);
this.nbBytes = nbBytes + CONTAINER_OVERHEAD_BYTES;
}
@Override
public int compareTo(CacheEntry o) {
// oldest first
return Long.compare(o.lastUsed, lastUsed);
}
}
private String getDisplayId(){
return "" + name + "-" + System.identityHashCode(this);
}
private void clearIfNeeded(){
if(timeIndex == Long.MAX_VALUE){
// reset long value...
timeIndex = 0;
cached.clear();
}else{
if(totalBytes > bytesLimit || cached.size() > entriesLimit ){
// sort by last used time, latest first
ArrayList<CacheEntry> sorted = new ArrayList<>();
for(CacheEntry o : cached.values()){
if(o.data.get()!=null){
sorted.add(o);
}
}
Collections.sort(sorted);
// Keep on adding until we reach half the bytes limit. This always keeps at least one object.
// Note if we decide to change this in the future to not cache any objects if the total size of every object is greater than the limit,
// we should update the component which does spatial queries against postcodes because for the UK postcode set,
// its quadtree will no longer be cached and performance will be very bad...
totalBytes = 0;
cached.clear();
int i =0;
while(i<sorted.size() && totalBytes < bytesLimit /2 && cached.size() < entriesLimit ){
cached.put(sorted.get(i).key, sorted.get(i));
totalBytes += sorted.get(i).nbBytes;
i++;
}
if(logToConsole){
System.out.println(getDisplayId() + " - cleared, total bytes now " + totalBytes+ " (" + (totalBytes/(1024*1024)) + " MB) in "+ cached.size() + " entries.");
}
// System.out.println("CLEARED");
}
}
}
@Override
public synchronized void put(Object objectKey, Object value, long nbBytes){
// remove the old object just in case it's already here so bytes count is correct
remove(objectKey);
timeIndex++;
CacheEntry obj = new CacheEntry(objectKey, value, nbBytes);
obj.lastUsed = timeIndex;
cached.put(objectKey, obj);
if(logToConsole){
long mb = totalBytes / (1024*1024);
long newMB = (totalBytes+obj.nbBytes) / (1024*1024);
if(mb!=newMB){
System.out.println(getDisplayId() + " - now " + (totalBytes+nbBytes) + " bytes (" + newMB + " MB) in " + cached.size() + " entries.");
}
}
totalBytes += obj.nbBytes;
clearIfNeeded();
}
@Override
public synchronized Object get(Object key){
CacheEntry c = cached.get(key);
if(c!=null){
c.lastUsed = timeIndex;
Object obj = c.data.get();
if(obj!=null){
return obj;
}else{
// collected already....
cached.remove(key);
totalBytes -= c.nbBytes;
}
}
return null;
}
public static void main(String []args){
RecentlyUsedCache lus = new RecentlyUsedCache("test",10*(8 + CacheEntry.CONTAINER_OVERHEAD_BYTES));
int n = 1000;
for(int i =0 ; i < n;i++){
Integer val = new Integer(i);
lus.put(val, val, 8);
lus.get(2);
System.out.println("i=" + i + " - " + lus.toString());
}
}
@Override
public synchronized void clear(){
timeIndex=0;
totalBytes=0;
cached.clear();
}
@Override
public synchronized String toString(){
StringBuilder builder = new StringBuilder();
builder.append("[");
int count=0;
for(Map.Entry<Object, CacheEntry> entry:cached.entrySet()){
Object val = entry.getValue().data.get();
if(val!=null){
if(count>0){
builder.append(", ");
}
builder.append("{" + entry.getKey() + "=" + val + "}");
count++;
}
}
builder.append("]");
return builder.toString();
}
/**
* Get snapshot of the keys and values stored in the cache.
* This could change directly after calling this method if anything
* is garbage collected. Calling this method does not update the last
* used state on the entries.
* @return
*/
public synchronized List<Pair<Object, Object>> getSnapshot(){
ArrayList<Pair<Object, Object>> ret = new ArrayList<>(cached.size());
for(Map.Entry<Object, CacheEntry> entry:cached.entrySet()){
Object val= entry.getValue().data.get();
if(val!=null){
ret.add(new Pair<Object, Object>(entry.getKey(), val));
}
}
return ret;
}
@Override
public synchronized void remove(Object key){
CacheEntry container = cached.get(key);
if(container!=null){
cached.remove(key);
totalBytes -= container.nbBytes;
}
}
public void setLogToConsole(boolean logToConsole) {
this.logToConsole = logToConsole;
}
public long getEstimatedTotalBytes(){
return totalBytes;
}
}