/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.github.geophile.erdo.immutableitemcache;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
// TODO: Could be package private except for referenceCountDistribution(), which is not essential
public class CacheEntryList<ID, ITEM extends CacheEntry<ID, ITEM>>
{
public int size()
{
return size;
}
public void addItem(ITEM item)
{
assert item != null;
assert !item.placeholder();
observer.adding(item);
item.recentAccess(true);
// In a non-empty list, the new item goes behind clockHand. Since it's newest, it's probably
// best to consider it for eviction last.
switch (size) {
case 0:
assert clockHand == null;
assert previous == null;
clockHand = item;
item.next(clockHand);
break;
case 1:
assert clockHand != null;
assert previous == null;
clockHand.next(item);
item.next(clockHand);
previous = item;
break;
default:
assert clockHand != null;
assert previous != null;
item.next(clockHand);
previous.next(item);
previous = item;
break;
}
size++;
}
public ITEM takeItemToEvict()
{
ITEM victim = null;
int count = 0;
int limit = 2 * size() + 1; // In a second pass, recentAccess will be false for all entries
if (clockHand != null) {
ITEM c = clockHand;
do {
c.recentAccess(false);
previous = c;
c = c.next();
if (++count > limit) {
logReferenceCounts();
throw new ImmutableItemCacheError("Unable to evict from cache!");
}
} while (c.recentAccess() || !c.okToEvict());
clockHand = c;
victim = clockHand;
observer.evicting(victim);
// Remove the victim
switch (size) {
case 0:
assert false;
break;
case 1:
clockHand = null;
previous = null;
break;
case 2:
clockHand = previous;
clockHand.next(clockHand);
previous = null;
break;
default:
clockHand = clockHand.next();
previous.next(clockHand);
break;
}
size--;
}
return victim;
}
public CacheEntryList(Observer<ID, ITEM> observer)
{
this.observer = observer;
}
// For testing
public Map<Integer, Integer> referenceCountDistribution()
{
Map<Integer, Integer> map = new TreeMap<>();
if (clockHand != null) {
ITEM c = clockHand;
do {
int referenceCount = c.referenceCount();
Integer occurrences = map.get(referenceCount);
if (occurrences == null) {
occurrences = 0;
}
map.put(referenceCount, occurrences + 1);
c = c.next();
} while (c != clockHand);
}
return map;
}
void clear()
{
size = 0;
clockHand = null;
previous = null;
}
// For use by this class
private void logReferenceCounts()
{
LOG.log(Level.SEVERE,
"Unable to evict from cache of size {0}. Reference count distibution:",
size());
Map<Integer, Integer> distribution = referenceCountDistribution();
for (Map.Entry<Integer, Integer> entry : distribution.entrySet()) {
int referenceCount = entry.getKey();
int occurrences = entry.getValue();
LOG.log(Level.SEVERE, " {0}: {1}", new Object[]{referenceCount, occurrences});
}
}
// Class state
private static final Logger LOG = Logger.getLogger(CacheEntryList.class.getName());
// Object state
private final Observer<ID, ITEM> observer;
private volatile int size = 0;
// clockHand is the clock hand of the clock algorithm. It is null only if and only if size = 0.
// previous trails the clock hand, so that the linked list can be maintained. It is null if and only
// if size = 1.
private volatile ITEM clockHand;
private volatile ITEM previous;
// Inner classes
public interface Observer<ID, ITEM extends CacheEntry<ID, ITEM>>
{
void adding(ITEM item);
void evicting(ITEM victim);
}
}