Skip to content

Commit

Permalink
Add the Quick Demotion - Lazy Promotion algorithm to the simulator
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-manes committed Jun 25, 2023
1 parent b525eb7 commit 59df43e
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
Expand All @@ -33,6 +34,7 @@
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyActor;
import com.github.benmanes.caffeine.cache.simulator.policy.Registry;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
Expand Down Expand Up @@ -76,9 +78,7 @@ public void run() {
broadcast(policies, trace);
report(policies, trace.characteristics());
} catch (RuntimeException e) {
if (!Thread.currentThread().isInterrupted()) {
throw e;
}
throwError(policies, e);
}
}

Expand All @@ -102,10 +102,11 @@ private void broadcast(List<PolicyActor> policies, TraceReader trace) {
var remainder = List.copyOf(batch);
for (var policy : policies) {
policy.send(remainder);
policy.finish();
}

var futures = policies.stream()
.map(PolicyActor::finish)
.map(PolicyActor::future)
.toArray(CompletableFuture<?>[]::new);
CompletableFuture.allOf(futures).join();
}
Expand Down Expand Up @@ -135,6 +136,25 @@ private ImmutableList<PolicyActor> getPolicyActors(Set<Characteristic> character
.collect(toImmutableList());
}

/** Throws the underlying cause for the simulation failure. */
private void throwError(ImmutableList<PolicyActor> policies, RuntimeException e) {
if (!Thread.currentThread().isInterrupted()) {
throw e;
}
for (var policy : policies) {
if (policy.future().isCompletedExceptionally()) {
try {
policy.future().join();
} catch (CompletionException error) {
Throwables.throwIfUnchecked(error.getCause());
error.addSuppressed(e);
throw error;
}
}
}
throw e;
}

public static void main(String[] args) {
Logger.getLogger("").setLevel(Level.WARNING);
var simulator = new Simulator(ConfigFactory.load());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ public void send(List<AccessEvent> events) {
}

/** Sends a shutdown signal after the pending messages are completed. */
public CompletableFuture<Void> finish() {
public void finish() {
submit(new Finish());
}

/** Return the future that signals the policy's completion. */
public CompletableFuture<Void> future() {
return future;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* 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.
* <p>
* This implementation is based on the code provided by the authors in their
* <a href="https://github.com/Thesys-lab/HotOS23-QD-LP">repository</a> and described by the paper
* <a href="https://jasony.me/publication/hotos23-qdlp.pdf">FIFO can be Better than LRU: the Power
* of Lazy Promotion and Quick Demotion</a> and the accompanying
* <a href="https://jasony.me/slides/hotos23-qdlp.pdf">slides</a>.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
@PolicySpec(name = "two-queue.Qdlp")
public final class QdlpPolicy implements KeyOnlyPolicy {
final Long2ObjectMap<Node> data;
final PolicyStats policyStats;
final Node headGhost;
final Node headFifo;
final Node headMain;

final int mainMaximumEntryFrequency;
final int moveToMainThreshold;
final int maximumSize;
final int maxGhost;
final int maxFifo;

int sizeGhost;
int sizeFifo;
int sizeMain;

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;
int freq = candidate.frequency;
candidate.frequency = 0;
candidate.remove();

if (freq >= moveToMainThreshold) {
evictFromMain();
candidate.appendToTail(headMain);
candidate.type = QueueType.MAIN;
sizeMain++;
sizeFifo--;
} else {
candidate.appendToTail(headGhost);
candidate.type = QueueType.GHOST;
candidate.frequency = 0;
sizeGhost++;
sizeFifo--;

if (sizeGhost > maxGhost) {
var ghost = headGhost.next;
data.remove(ghost.key);
ghost.remove();
sizeGhost--;
}
}
}

private void evictFromMain() {
for (;;) {
Node victim = headMain.next;
if (victim.frequency == 0) {
data.remove(victim.key);
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");
}
}
}
Loading

0 comments on commit 59df43e

Please sign in to comment.