/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2016 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.exist.storage.cache;
import net.jcip.annotations.NotThreadSafe;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.storage.CacheManager;
import org.exist.util.hashtable.Long2ObjectHashMap;
import java.lang.reflect.Array;
/**
* Cache implementation based on the GClock algorithm.
*
* Implements a mixture between LFU (Last Frequently Used) and LRU
* (Last Recently Used) replacement policies. The class
* uses reference counts to track references to cached objects. Each call to the add
* method increments the reference count of the object.
*
* If the cache is full, the object to be removed is determined by decrementing the
* reference count for each object until an object with reference count = 0 is found.
*
* The implementation tends to replace younger objects first.
*
* @author wolf
*/
@NotThreadSafe
public class GClockCache<T extends Cacheable> implements Cache<T> {
private final static Logger LOG = LogManager.getLogger(GClockCache.class);
private final String name;
private final Class<T> cacheableClazz;
protected int size;
private final double growthFactor;
Accounting accounting;
private final String type;
protected T[] items;
protected Long2ObjectHashMap<T> map;
protected int count = 0;
protected int used = 0;
private int hitsOld = 0;
protected CacheManager cacheManager = null;
public GClockCache(final String name, final Class<T> cacheableClazz, final int size, final double growthFactor, final double growthThreshold, final String type) {
this.name = name;
this.cacheableClazz = cacheableClazz;
this.size = size;
this.growthFactor = growthFactor;
accounting = new Accounting(growthThreshold);
accounting.setTotalSize(size);
this.type = type;
this.items = createArray(cacheableClazz, size);
this.map = new Long2ObjectHashMap<>(size * 2);
}
@SuppressWarnings("unchecked")
private T[] createArray(final Class<T> clazz, final int size) {
return (T[])Array.newInstance(clazz, size);
}
@Override
public String getName() {
return name;
}
@Override
public String getType() {
return type;
}
@Override
public void add(final T item) {
add(item, 1);
}
@Override
public void add(final T item, final int initialRefCount) {
final T old = map.get(item.getKey());
if (old != null) {
old.incReferenceCount();
return;
}
item.setReferenceCount(initialRefCount);
if (count < size) {
items[count++] = item;
map.put(item.getKey(), item);
used++;
} else {
removeOne(item);
}
}
@Override
public T get(final T item) {
return get(item.getKey());
}
@Override
public T get(final long key) {
final T item = map.get(key);
if (item == null) {
accounting.missesIncrement();
} else {
accounting.hitIncrement();
}
return item;
}
@Override
public void remove(final T item) {
final long key = item.getKey();
final T cacheable = map.remove(key);
if (cacheable == null) {
return;
}
for (int i = 0; i < count; i++) {
if (items[i] != null && items[i].getKey() == key) {
items[i] = null;
used--;
return;
}
}
LOG.error("item not found in list");
}
@Override
public boolean flush() {
boolean flushed = false;
int written = 0;
for (int i = 0; i < count; i++) {
if (items[i] != null && items[i].sync(false)) {
++written;
flushed = true;
}
}
if(LOG.isTraceEnabled()) {
LOG.trace(written + " pages written to disk");
}
return flushed;
}
@Override
public boolean hasDirtyItems() {
for(int i = 0; i < count; i++) {
if(items[i] != null && items[i].isDirty()) {
return true;
}
}
return false;
}
protected T removeOne(final T item) {
T old = null;
boolean removed = false;
int bucket;
do {
bucket = -1;
// decrease all reference counts by 1
for (int i = 0; i < count; i++) {
old = items[i];
if (old == null) {
bucket = i;
} else if (old.decReferenceCount() < 1 && bucket < 0) {
bucket = i;
}
}
if (bucket > -1) {
old = items[bucket];
if (old != null) {
if(LOG.isTraceEnabled()) {
LOG.trace(name + " replacing " + old.getKey() + " for " + item.getKey());
}
map.remove(old.getKey());
old.sync(true);
} else {
used++;
}
items[bucket] = item;
map.put(item.getKey(), item);
removed = true;
}
} while (!removed);
if (old != null) {
accounting.replacedPage(item);
if (cacheManager != null && accounting.resizeNeeded()) {
cacheManager.requestMem(this);
}
}
return old;
}
@Override
public int getBuffers() {
return size;
}
@Override
public int getUsedBuffers() {
return used;
}
@Override
public double getGrowthFactor() {
return growthFactor;
}
@Override
public int getHits() {
return accounting.getHits();
}
@Override
public int getFails() {
return accounting.getMisses();
}
public int getThrashing() {
return accounting.getThrashing();
}
@Override
public void setCacheManager(final CacheManager manager) {
this.cacheManager = manager;
}
@Override
public void resize(final int newSize) {
if (newSize < size) {
shrink(newSize);
} else {
final T[] newItems = createArray(cacheableClazz, newSize);
final Long2ObjectHashMap<T> newMap = new Long2ObjectHashMap<>(newSize * 2);
for (int i = 0; i < count; i++) {
newItems[i] = items[i];
newMap.put(items[i].getKey(), items[i]);
}
this.size = newSize;
this.map = newMap;
this.items = newItems;
accounting.reset();
accounting.setTotalSize(size);
}
}
private void shrink(final int newSize) {
flush();
items = createArray(cacheableClazz, newSize);
map = new Long2ObjectHashMap<>(newSize * 2);
size = newSize;
count = 0;
used = 0;
accounting.reset();
accounting.setTotalSize(size);
}
@Override
public int getLoad() {
if (hitsOld == 0) {
hitsOld = accounting.getHits();
return Integer.MAX_VALUE;
}
final int load = accounting.getHits() - hitsOld;
hitsOld = accounting.getHits();
return load;
}
}