/*
* Copyright 2003-2014 JetBrains s.r.o.
*
* 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 jetbrains.mps.util;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* User: fyodor
* Date: 8/27/12
*/
public class SimpleLRUCache<K> {
private static final int DEFAULT_MAX_SIZE = 20000;
private static final double FIRST_LEVEL_RATIO = 0.6;
private static final double CLEANUP_Q1_RATIO = 0.06;
private final AtomicInteger roomLeftFirstLevel;
private final AtomicInteger roomLeftSecondLevel;
private final ConcurrentHashMap<K, K> firstLevelCache; // aka L1
private final ConcurrentLinkedQueue<K> firstLevelQueue = new ConcurrentLinkedQueue<K>(); // aka Q1
private final ConcurrentHashMap<K, K> secondLevelCache; // aka L2
private final ConcurrentLinkedQueue<K> secondLevelQueue = new ConcurrentLinkedQueue<K>(); // aka Q2
private final ConcurrentHashMap<K, K> transitionalCache = new ConcurrentHashMap<K, K>();
private int promotesBeforeCleanupInitialValue;
private final AtomicInteger promotesBeforeCleanup;
public SimpleLRUCache(int maxSize) {
final int sizeL1 = (int) (maxSize * FIRST_LEVEL_RATIO);
final int sizeL2 = (int) (maxSize * (1. - FIRST_LEVEL_RATIO));
roomLeftFirstLevel = new AtomicInteger(sizeL1);
roomLeftSecondLevel = new AtomicInteger(sizeL2);
// compensate HashMap size for default load factor of 0.75
firstLevelCache = new ConcurrentHashMap<K, K>(sizeL1 * 4 / 3);
secondLevelCache = new ConcurrentHashMap<K, K>(sizeL2 * 4 / 3);
promotesBeforeCleanupInitialValue = (int) (maxSize * CLEANUP_Q1_RATIO);
promotesBeforeCleanup = new AtomicInteger(promotesBeforeCleanupInitialValue);
}
public SimpleLRUCache() {
this(DEFAULT_MAX_SIZE);
}
public int size() {
return firstLevelCache.size() + secondLevelCache.size();
}
protected K canonic(K k) {
return k;
}
protected void purged(K k) {}
@Override
public String toString() {
return "LRU["+firstLevelCache.size()+", "+secondLevelCache.size()+"]";
}
public K cacheObject (K toCache) {
K cached = secondLevelCache.get(toCache);
if (cached != null) {
return cached;
}
cached = firstLevelCache.get(toCache);
if (cached != null) {
return primPromote(cached);
}
cached = transitionalCache.get(toCache);
if (cached != null) {
return cached;
}
return primCacheObject(canonic(toCache));
}
private K primPromote(K cached) {
if (lock(cached)) {
// current thread has a mutex on 'cached'
K alreadyPromoted = secondLevelCache.putIfAbsent(cached, cached);
if (alreadyPromoted != null) {
unlock(cached);
return alreadyPromoted;
}
secondLevelQueue.add(cached);
if (firstLevelCache.remove(cached, cached)) {
roomLeftFirstLevel.incrementAndGet();
if (promotesBeforeCleanup.decrementAndGet() <= 0) {
cleanupQ1();
}
}
if (roomLeftSecondLevel.decrementAndGet() <= 0) {
K toDemote = secondLevelQueue.poll();
assert toDemote != null;
primCacheObject(toDemote);
if (lock(toDemote)) {
if (secondLevelCache.remove(toDemote, toDemote)) {
roomLeftSecondLevel.incrementAndGet();
}
unlock(toDemote);
} else {
secondLevelQueue.add(toDemote);
}
}
unlock(cached);
}
return cached;
}
private K primCacheObject(K canonic) {
if (lock(canonic)) {
// current thread has a mutex on 'canonic'
K alreadyCached = firstLevelCache.putIfAbsent(canonic, canonic);
if (alreadyCached != null) {
unlock(canonic);
return alreadyCached;
}
firstLevelQueue.add(canonic);
if (roomLeftFirstLevel.decrementAndGet() <= 0) {
K toRemove;
do {
toRemove = firstLevelQueue.poll();
assert toRemove != null;
} while (!firstLevelCache.containsKey(toRemove));
if (lock(toRemove)) {
if (firstLevelCache.remove(toRemove, toRemove)) {
roomLeftFirstLevel.incrementAndGet();
purged(toRemove);
}
unlock(toRemove);
} else {
firstLevelQueue.add(toRemove);
}
}
unlock(canonic);
}
return canonic;
}
private boolean lock(K cached) {
return transitionalCache.putIfAbsent(cached, cached) == null;
}
private void unlock(K cached) {
boolean removed = transitionalCache.remove(cached, cached);
assert removed;
}
/**
* Unlike L2 and Q2, L1 elements can be removed independently from elements in Q1, when promoting L1 element to L2.
* Afterwards, when L2 elements 'demoted' back to L1 get added to Q1, there are duplicating queue elements.
* The Q1 is cleaned up after the number o promotes hits the threshold.
*/
private void cleanupQ1() {
promotesBeforeCleanup.set(promotesBeforeCleanupInitialValue);
for (Iterator<K> it = firstLevelQueue.iterator(); it.hasNext();) {
if (!firstLevelCache.containsKey(it.next())) {
it.remove();
}
}
}
}