diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java
index 6decc378ce..39ae09cbd6 100644
--- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java
+++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java
@@ -114,6 +114,7 @@ private abstract class Command implements Runnable {
try {
execute();
} catch (Throwable t) {
+ t.printStackTrace();
parent.interrupt();
throw t;
} finally {
diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java
index 359336d193..8ea052b47b 100644
--- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java
+++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java
@@ -72,6 +72,7 @@
import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.TinyCachePolicy;
import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.TinyCacheWithGhostCachePolicy;
import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.WindowTinyCachePolicy;
+import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.QdlpPolicy;
import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.TuQueuePolicy;
import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.TwoQueuePolicy;
import com.google.auto.value.AutoValue;
@@ -171,6 +172,7 @@ private void registerSampled() {
}
private void registerTwoQueue() {
+ register(QdlpPolicy.class, QdlpPolicy::new);
register(TuQueuePolicy.class, TuQueuePolicy::new);
register(TwoQueuePolicy.class, TwoQueuePolicy::new);
}
diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java
new file mode 100644
index 0000000000..6687c5414d
--- /dev/null
+++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2023 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 com.github.benmanes.caffeine.cache.simulator.BasicSettings;
+import com.github.benmanes.caffeine.cache.simulator.policy.Policy.KeyOnlyPolicy;
+import com.github.benmanes.caffeine.cache.simulator.policy.Policy.PolicySpec;
+import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
+import com.google.common.base.MoreObjects;
+import com.typesafe.config.Config;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+
+/**
+ * The Quick Demotion - Lazy Promotion algorithm. This algorithm uses a probationary FIFO queue to
+ * evaluate whether a recent arrival should be admitted into the main region based on a frequency
+ * threshold. A rejected candidate is placed into a ghost cache, where a cache miss that hits in the
+ * ghost cache will cause the entry to be immediately promoted into the main region. This admission
+ * scheme is referred to as "quick demotion" by the authors. The main region uses an n-bit clock
+ * eviction policy, which is referred to as "lazy promotion" by the authors.
+ *
+ * This implementation is based on the code provided by the authors in their
+ * repository and described by the paper
+ * FIFO can be Better than LRU: the Power
+ * of Lazy Promotion and Quick Demotion and the accompanying
+ * slides.
+ *
+ * @author ben.manes@gmail.com (Ben Manes)
+ */
+@PolicySpec(name = "two-queue.Qdlp")
+public final class QdlpPolicy implements KeyOnlyPolicy {
+ final Long2ObjectMap data;
+ final PolicyStats policyStats;
+ final Node headGhost;
+ final Node headFifo;
+ final Node headMain;
+
+ final int mainMaximumEntryFrequency;
+ final int maximumSize;
+ final int maxGhost;
+ final int maxFifo;
+
+ int sizeFifo;
+ int sizeMain;
+ int sizeGhost;
+ int moveToMainThreshold;
+
+ public QdlpPolicy(Config config) {
+ QdlpSettings settings = new QdlpSettings(config);
+ this.data = new Long2ObjectOpenHashMap<>();
+ this.policyStats = new PolicyStats(name());
+ this.headGhost = new Node();
+ this.headFifo = new Node();
+ this.headMain = new Node();
+
+ this.moveToMainThreshold = settings.moveToMainThreshold();
+ this.maximumSize = Math.toIntExact(settings.maximumSize());
+ this.maxFifo = (int) (maximumSize * settings.percentFifo());
+ this.maxGhost = (int) (maximumSize * settings.percentGhost());
+ this.mainMaximumEntryFrequency = settings.mainMaximumEntryFrequency();
+ }
+
+ @Override
+ public void record(long key) {
+ policyStats.recordOperation();
+ Node node = data.get(key);
+ if (node == null) {
+ onMiss(key);
+ } else if (node.type == QueueType.GHOST) {
+ onGhostHit(node);
+ } else {
+ onHit(node);
+ }
+ }
+
+ private void onHit(Node node) {
+ if (node.type == QueueType.FIFO) {
+ node.frequency++;
+ } else if (node.type == QueueType.MAIN) {
+ node.frequency = Math.min(node.frequency + 1, mainMaximumEntryFrequency);
+ }
+ policyStats.recordHit();
+ }
+
+ private void onGhostHit(Node node) {
+ policyStats.recordMiss();
+ node.remove();
+ sizeGhost--;
+
+ node.appendToTail(headMain);
+ node.type = QueueType.MAIN;
+ sizeMain++;
+ evict();
+ }
+
+ private void onMiss(long key) {
+ Node node = new Node(key);
+ node.appendToTail(headFifo);
+ node.type = QueueType.FIFO;
+ policyStats.recordMiss();
+ data.put(key, node);
+ sizeFifo++;
+ evict();
+
+ if (sizeFifo > maxFifo) {
+ Node promoted = headFifo.next;
+ promoted.remove();
+ sizeFifo--;
+
+ promoted.appendToTail(headMain);
+ promoted.type = QueueType.MAIN;
+ sizeMain++;
+ }
+ }
+
+ private void evict() {
+ if ((sizeFifo + sizeMain) <= maximumSize) {
+ return;
+ }
+ policyStats.recordEviction();
+
+ if ((maxFifo == 0) || (sizeFifo == 0)) {
+ evictFromMain();
+ return;
+ }
+
+ Node candidate = headFifo.next;
+ checkState(data.containsKey(candidate.key), candidate);
+ int freq = candidate.frequency;
+ candidate.frequency = 0;
+ candidate.remove();
+
+ if (freq >= moveToMainThreshold) {
+ evictFromMain();
+ candidate.appendToTail(headMain);
+ candidate.type = QueueType.MAIN;
+ sizeMain++;
+ sizeFifo--;
+ checkState(data.size() <= (maximumSize + maxGhost),
+ "%s > %s", data.size(), (maximumSize + maxGhost));
+
+ } else {
+ candidate.appendToTail(headGhost);
+ candidate.type = QueueType.GHOST;
+ candidate.frequency = 0;
+ sizeGhost++;
+ sizeFifo--;
+
+ if (sizeGhost > maxGhost) {
+ var ghost = headGhost.next;
+ checkState(data.remove(ghost.key) != null);
+ ghost.remove();
+ sizeGhost--;
+ }
+ checkState(data.size() <= (maximumSize + maxGhost),
+ "%s > %s", data.size(), (maximumSize + maxGhost));
+ }
+ }
+
+ private void evictFromMain() {
+ for (;;) {
+ Node victim = headMain.next;
+ if (victim.frequency == 0) {
+ checkState(data.remove(victim.key) != null);
+ victim.remove();
+ sizeMain--;
+ break;
+ }
+ victim.frequency--;
+ victim.moveToTail(headMain);
+ }
+ }
+
+ @Override
+ public void finished() {
+ int maximum = (maximumSize + maxGhost);
+ checkState(data.size() <= maximum, "%s > %s", data.size(), maximum);
+
+ long ghosts = data.values().stream().filter(node -> node.type == QueueType.GHOST).count();
+ checkState(ghosts == sizeGhost, "ghosts: %s != %s", ghosts, sizeGhost);
+ checkState(ghosts <= maxGhost, "ghosts: %s > %s", ghosts, maxGhost);
+ }
+
+ @Override
+ public PolicyStats stats() {
+ return policyStats;
+ }
+
+ enum QueueType {
+ FIFO,
+ MAIN,
+ GHOST,
+ }
+
+ static final class Node {
+ final long key;
+
+ Node prev;
+ Node next;
+ QueueType type;
+ int frequency;
+
+ 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) {
+ checkState(prev == null);
+ checkState(next == null);
+
+ 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) {
+ checkState(prev != null);
+ checkState(next != null);
+
+ // 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(prev != null);
+ checkState(next != null);
+
+ prev.next = next;
+ next.prev = prev;
+ prev = next = null;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("key", key)
+ .add("type", type)
+ .toString();
+ }
+ }
+
+ static final class QdlpSettings extends BasicSettings {
+ public QdlpSettings(Config config) {
+ super(config);
+ }
+ public double percentFifo() {
+ return config().getDouble("qdlp.percent-fifo");
+ }
+ public double percentGhost() {
+ return config().getDouble("qdlp.percent-ghost");
+ }
+ public int moveToMainThreshold() {
+ return config().getInt("qdlp.move-to-main-threshold");
+ }
+ public int mainMaximumEntryFrequency() {
+ return config().getInt("qdlp.main-clock-maximum-frequency");
+ }
+ }
+}
diff --git a/simulator/src/main/resources/reference.conf b/simulator/src/main/resources/reference.conf
index f86da8b8af..201d7cb851 100644
--- a/simulator/src/main/resources/reference.conf
+++ b/simulator/src/main/resources/reference.conf
@@ -66,6 +66,7 @@ caffeine.simulator {
# Policies based on the 2Q algorithm
two-queue.TwoQueue,
two-queue.TuQueue,
+ two-queue.Qdlp,
# Policies based on a sketch algorithm
sketch.WindowTinyLfu,
@@ -176,6 +177,17 @@ caffeine.simulator {
percent-warm = 0.33
}
+ qdlp {
+ # The percentage for the FIFO queue
+ percent-fifo = 0.10
+ # The percentage for the GHOST queue
+ percent-ghost = 0.90
+ # The promotion frequency threshold
+ move-to-main-threshold = 1
+ # The n-bit clock frequency for the MAIN queue
+ main-clock-maximum-frequency = 1
+ }
+
tiny-lfu {
# CountMinSketch: count-min-4 (4-bit), count-min-64 (64-bit)
# Table: random-table, tiny-table, perfect-table