/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* 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 net.ontopia.utils;
/**
* INTERNAL: A LookupIndexIF which uses another, slower, LookupIndexIF
* as a fallback and caches the values attached to the most commonly
* requested keys using an LRU strategy. There is a maximum number of
* keys that can be stored in the index and the index will
* automatically prune the less-used keys to avoid the index growing
* above this maximum size.
*/
public class CachedIndex<K, E> implements LookupIndexIF<K, E> {
private LookupIndexIF<K, E> fallback;
private int max; // max number of entries in cache
private int entries; // current number of entries in cache
private int decay; // how many hits to decay hit count by
// when pruning. (those with less removed.)
private Entry[] data; // current entries
private double threshold; // will rehash when entries/data.len>thresh
private boolean nulls; // store nulls retrieved from fallback.
// statistics collection
private long lookups;
private long hits;
private long rehashes;
private long prunings;
/**
* Creates an index with the given fallback and default settings.
*/
public CachedIndex(LookupIndexIF<K, E> fallback) {
this.fallback = fallback;
this.max = 10000;
this.data = new Entry[1001];
this.entries = 0;
this.threshold = 0.25;
this.decay = 10;
this.nulls = true;
}
/**
* Creates an index with the given fallback, default settings and
* the specified nulls setting.
*/
public CachedIndex(LookupIndexIF<K, E> fallback, boolean nulls) {
this(fallback);
this.nulls = nulls;
}
/**
* Creates an index with the given fallback and settings.
* @param fallback The index to ask if the value is not found in the cache.
* @param max The max number of keys to store in the cache (default: 10000).
* @param size The initial size of the cache.
* @param nulls Store null values retrieved from fallback.
*/
public CachedIndex(LookupIndexIF<K, E> fallback, int max, int size, boolean nulls) {
this.fallback = fallback;
this.max = max;
this.data = new Entry[size];
this.entries = 0;
this.threshold = 0.25;
this.decay = 10;
this.nulls = nulls;
}
public E get(K key) {
Entry entry = data[(key.hashCode() & 0x7FFFFFFF) % data.length];
while (entry != null && !entry.key.equals(key))
entry = entry.next;
lookups++;
if (entry == null) { // not found
E result = fallback.get(key);
if (result == null && !nulls) return null; // do not store null values
entry = addEntry(new Entry(key, result));
} else {
hits++;
entry.hits++;
}
return (E) entry.value;
}
public E put(K key, E value) {
// check if key already there; otherwise may end up with two entries
// with same key
Entry entry = data[(key.hashCode() & 0x7FFFFFFF) % data.length];
while (entry != null && !entry.key.equals(key))
entry = entry.next;
if (entry == null)
addEntry(new Entry(key, value));
else
entry.value = value;
return value;
}
public E remove(K key) {
int ix = (key.hashCode() & 0x7FFFFFFF) % data.length;
Entry<K, E> entry = data[ix];
Entry<K, E> previous = null;
while (entry != null) {
if (entry.key.equals(key)) {
// FIXME: pass on news to fallback?
if (previous == null)
data[ix] = entry.next;
else
previous.next = entry.next;
entries--;
return (E)entry.value;
}
previous = entry;
entry = entry.next;
}
return null;
}
// --- Extra methods
public int getKeyNumber() {
return entries;
}
public void writeReport() {
System.out.println("--- CachedIndex report");
System.out.println("lookups: " + lookups);
System.out.println("misses: " + (lookups - hits));
System.out.println("ratio: " + (((float) hits) / lookups));
System.out.println("array size: " + data.length);
System.out.println("keys: " + entries);
System.out.println("rehashes: " + rehashes);
System.out.println("prunings: " + prunings);
}
// --- Internal methods
/**
* Called to add an entry object into the data array. Assumes that
* no entry with the same key already exists in the data array.
*/
private Entry<K, E> addEntry(Entry<K, E> newEntry) {
if (entries >= max)
prune();
else if (((float) entries) / data.length > threshold)
rehash(data.length*2 + 1);
int ix = (newEntry.key.hashCode() & 0x7FFFFFFF) % data.length;
if (data[ix] == null)
data[ix] = newEntry;
else {
newEntry.next = data[ix];
data[ix] = newEntry;
}
entries++;
return newEntry;
}
/**
* Removes some of the keys in the cache, keeping only the most
* frequently requested keys.
*/
protected void prune() {
prunings++;
// System.out.println("PRUNING! Keys now: " + entries);
for (int ix = 0; ix < data.length; ix++) {
Entry current = data[ix];
Entry previous = null;
while (current != null) {
if (current.hits < decay) {
if (previous == null)
data[ix] = current.next;
else
previous.next = current.next;
entries--;
} else
current.hits -= decay;
current = current.next;
}
}
// System.out.println("Done. Keys now: " + entries);
}
/**
* Increases the size of the data array to the given size.
*/
private void rehash(int size) {
rehashes++;
// System.out.println("rehashing to: " + size + " keys: " + entries);
Entry[] olddata = data;
data = new Entry[size];
for (int ix = 0; ix < olddata.length; ix++) {
Entry current = olddata[ix];
while (current != null) {
Entry next = current.next;
current.next = null;
// copied from addEntry
int pos = (current.key.hashCode() & 0x7FFFFFFF) % data.length;
if (data[pos] == null)
data[pos] = current;
else {
current.next = data[pos];
data[pos] = current;
}
// end of copy
current = next;
}
}
}
// --- Internal Entry class
public class Entry<A, B> {
public Object value;
public Object key;
public int hits;
public Entry<A, B> next;
public Entry(A key, B value) {
this.key = key;
this.value = value;
this.hits = 1;
}
}
}