/*
* Copyright 2015 Ben Manes. All Rights Reserved.
*
* 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 com.github.benmanes.caffeine.cache.simulator.policy.two_queue;
import static com.google.common.base.Preconditions.checkState;
import java.util.Set;
import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.policy.Policy;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import com.typesafe.config.Config;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
/**
* An adaption of the 2Q algorithm used by OpenBSD and memcached. Unlike the original 2Q algorithm,
* non-resident entries are not retained in the TU-Q policy. For details see OpenBSD description at
* <a href="http://www.tedunangst.com/flak/post/2Q-buffer-cache-algorithm">2Q buffer cache
* algorithm</a> and memcached's at <a href="https://github.com/memcached/memcached/pull/97">[Work
* In Progress] LRU rework</a>.
* <p>
* "We retain the key three working set distinction. In the OpenBSD code, they are named hot, cold,
* and warm, and each is an LRU queue. New buffers start hot. They stay that way as long as they
* remain on the hot queue. Eventually, a buffer will slip from the end of the hot queue onto the
* front of the cold queue. (We preserve the data, not just the address.) When a new buffer is
* needed, we recycle one from the tail of the cold queue. The oldest and coldest. If, on the other
* hand, we have a cache hit on a cold buffer, it turns into a warm buffer and goes to the front of
* the warm queue. Then as the warm queue lengthens, buffers start slipping from the end onto the
* cold queue. Both the hot and warm queues are capped at one third of memory each to ensure
* balance."
* <p>
* Scan resistance is achieved by means of the warm queue. Transient data will pass from hot queue
* to cold queue and be recycled. Responsiveness is maintained by making the warm queue LRU so that
* expired long term set buffers fade away."
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public class TuQueuePolicy implements Policy {
private final Long2ObjectMap<Node> data;
private final PolicyStats policyStats;
private final int maximumSize;
private int sizeHot;
private final int maxHot;
private final Node headHot;
private int sizeWarm;
private final int maxWarm;
private final Node headWarm;
private int sizeCold;
private final Node headCold;
public TuQueuePolicy(Config config) {
TuQueueSettings settings = new TuQueueSettings(config);
this.headHot = new Node();
this.headWarm = new Node();
this.headCold = new Node();
this.maximumSize = settings.maximumSize();
this.data = new Long2ObjectOpenHashMap<>();
this.policyStats = new PolicyStats("two-queue.TuQueue");
this.maxHot = (int) (maximumSize * settings.percentHot());
this.maxWarm = (int) (maximumSize * settings.percentWarm());
}
/** Returns all variations of this policy based on the configuration parameters. */
public static Set<Policy> policies(Config config) {
return ImmutableSet.of(new TuQueuePolicy(config));
}
@Override
public void record(long key) {
policyStats.recordOperation();
Node node = data.get(key);
if (node == null) {
policyStats.recordMiss();
onMiss(key);
} else {
policyStats.recordHit();
onHit(node);
}
}
private void onHit(Node node) {
if (node.type == QueueType.HOT) {
node.moveToTail(headHot);
} else if (node.type == QueueType.WARM) {
node.moveToTail(headWarm);
} else if (node.type == QueueType.COLD) {
// If we have a cache hit on a cold buffer, it turns into a warm buffer and goes to the
// front of the warm queue. Then as the warm queue lengthens, buffers start slipping from
// the end onto the cold queue.
node.remove();
sizeCold--;
node.type = QueueType.WARM;
node.appendToTail(headWarm);
sizeWarm++;
if (sizeWarm > maxWarm) {
Node demoted = headWarm.next;
demoted.remove();
sizeWarm--;
demoted.type = QueueType.COLD;
demoted.appendToTail(headCold);
sizeCold++;
}
} else {
throw new IllegalStateException();
}
}
/** Adds the entry to the cache as HOT, overflowing to the COLD queue, and evicts if necessary. */
private void onMiss(long key) {
Node node = new Node(key);
node.type = QueueType.HOT;
node.appendToTail(headHot);
data.put(key, node);
sizeHot++;
if (sizeHot > maxHot) {
Node demoted = headHot.next;
demoted.remove();
sizeHot--;
demoted.appendToTail(headCold);
demoted.type = QueueType.COLD;
sizeCold++;
evict();
}
}
private void evict() {
if (data.size() > maximumSize) {
Node victim = headCold.next;
data.remove(victim.key);
victim.remove();
sizeCold--;
policyStats.recordEviction();
}
}
@Override
public PolicyStats stats() {
return policyStats;
}
@Override
public void finished() {
checkState(sizeHot + sizeWarm + sizeCold == data.size());
}
enum QueueType {
HOT,
WARM,
COLD,
}
static final class Node {
final long key;
Node prev;
Node next;
QueueType type;
Node() {
this.key = Long.MIN_VALUE;
this.prev = this;
this.next = this;
}
Node(long key) {
this.key = key;
}
/** Appends the node to the tail of the list. */
public void appendToTail(Node head) {
Node tail = head.prev;
head.prev = this;
tail.next = this;
next = head;
prev = tail;
}
/** Moves the node to the tail. */
public void moveToTail(Node head) {
// unlink
prev.next = next;
next.prev = prev;
// link
next = head;
prev = head.prev;
head.prev = this;
prev.next = this;
}
/** Removes the node from the list. */
public void remove() {
checkState(key != Long.MIN_VALUE);
prev.next = next;
next.prev = prev;
prev = next = null;
type = null;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("key", key)
.add("type", type)
.toString();
}
}
static final class TuQueueSettings extends BasicSettings {
public TuQueueSettings(Config config) {
super(config);
}
public double percentHot() {
double percentHot = config().getDouble("tu-queue.percent-hot");
checkState(percentHot < 1.0);
return percentHot;
}
public double percentWarm() {
double percentWarm = config().getDouble("tu-queue.percent-warm");
checkState(percentWarm < 1.0);
return percentWarm;
}
}
}