/*
* 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.linked;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import java.util.Arrays;
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.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectSortedMap;
/**
* The MultiQueue algorithm. This algorithm organizes entries into queues that represent a frequency
* range. When an entry is accessed, it may be promoted to the next higher queue and, regardless, is
* reordered to the least-recently-used position in the queue it resides in. A non-resident queue
* retains evicted items that are being monitored (OUT) to allow entries to retain their historic
* frequency and be eagerly promoted.
* <p>
* This policy is designed for second-level caches where a hit in this cache was a miss at the first
* level. Thus the first-level cache captures most of the recency information and the second-level
* cache access is dominated by usage frequency.
* <p>
* This implementation is based on the pseudo code provided by the authors in their paper
* <a href="https://www.usenix.org/legacy/event/usenix01/full_papers/zhou/zhou.pdf">The Multi-Queue
* Replacement Algorithm for Second Level. Buffer Caches</a>.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class MultiQueuePolicy implements Policy {
private final Long2ObjectSortedMap<Node> out;
private final Long2ObjectMap<Node> data;
private final PolicyStats policyStats;
private final long[] threshold;
private final int maximumSize;
private final long lifetime;
private final Node[] headQ;
private final int maxOut;
private long currentTime;
public MultiQueuePolicy(Config config) {
MultiQueueSettings settings = new MultiQueueSettings(config);
policyStats = new PolicyStats("linked.MultiQueue");
threshold = new long[settings.numberOfQueues()];
headQ = new Node[settings.numberOfQueues()];
out = new Long2ObjectLinkedOpenHashMap<>();
data = new Long2ObjectOpenHashMap<>();
maximumSize = settings.maximumSize();
lifetime = settings.lifetime();
Arrays.setAll(headQ, Node::sentinel);
Arrays.setAll(threshold, i -> 1L << i);
maxOut = (int) (maximumSize * settings.percentOut());
}
/** Returns all variations of this policy based on the configuration parameters. */
public static Set<Policy> policies(Config config) {
return ImmutableSet.of(new MultiQueuePolicy(config));
}
@Override
public void record(long key) {
policyStats.recordOperation();
Node node = data.get(key);
if (node == null) {
policyStats.recordMiss();
node = out.remove(key);
if (node == null) {
node = new Node(key);
}
data.put(key, node);
if (data.size() > maximumSize) {
policyStats.recordEviction();
evict();
}
} else {
policyStats.recordHit();
node.remove();
}
node.reference++;
node.queueIndex = queueIndexFor(node);
node.appendToTail(headQ[node.queueIndex]);
node.expireTime = currentTime + lifetime;
adjust();
}
private void adjust() {
currentTime++;
for (int i = 1; i < headQ.length; i++) {
Node node = headQ[i].next;
if (node.next.expireTime < currentTime) {
node.remove();
node.queueIndex = (i - 1);
node.appendToTail(headQ[node.queueIndex]);
node.expireTime = currentTime + lifetime;
}
}
}
private int queueIndexFor(Node node) {
for (int i = threshold.length - 1; i >= 0; i--) {
if (node.reference >= threshold[i]) {
return i;
}
}
throw new IllegalStateException();
}
private void evict() {
Node victim = null;
for (Node head : headQ) {
if (head.next != head) {
victim = head.next;
break;
}
}
if (victim == null) {
return;
}
victim.remove();
data.remove(victim.key);
out.put(victim.key, victim);
if (out.size() > maxOut) {
out.remove(out.firstLongKey());
}
}
@Override
public PolicyStats stats() {
return policyStats;
}
static final class Node {
final long key;
Node prev;
Node next;
int reference;
int queueIndex;
long expireTime;
Node(long key) {
this.key = key;
}
static Node sentinel(int queueIndex) {
Node node = new Node(Long.MIN_VALUE);
node.expireTime = Long.MAX_VALUE;
node.queueIndex = queueIndex;
node.prev = node;
node.next = node;
return node;
}
/** 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;
}
/** Removes the node from the list. */
public void remove() {
checkState(key != Long.MIN_VALUE);
queueIndex = -1;
prev.next = next;
next.prev = prev;
prev = next = null;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("key", key)
.add("references", reference)
.toString();
}
}
static final class MultiQueueSettings extends BasicSettings {
public MultiQueueSettings(Config config) {
super(config);
}
public int lifetime() {
return config().getInt("multi-queue.lifetime");
}
public int numberOfQueues() {
int queues = config().getInt("multi-queue.num-queues");
checkArgument(queues > 0, "Must have one or more queues");
checkArgument(queues <= 62, "May not have more than 62 queues");
return queues;
}
public double percentOut() {
return config().getDouble("multi-queue.percent-out");
}
}
}