/* * Copyright 2016 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.sketch.climbing; import static com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.Adaptation.Type.DECREASE_WINDOW; import static com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.Adaptation.Type.INCREASE_WINDOW; import static com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.QueueType.PROBATION; import static com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.QueueType.PROTECTED; import static com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.QueueType.WINDOW; import static com.google.common.base.Preconditions.checkState; import static java.util.stream.Collectors.toSet; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import com.github.benmanes.caffeine.cache.simulator.BasicSettings; import com.github.benmanes.caffeine.cache.simulator.admission.Admittor; import com.github.benmanes.caffeine.cache.simulator.admission.TinyLfu; import com.github.benmanes.caffeine.cache.simulator.policy.Policy; import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats; import com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.Adaptation; import com.github.benmanes.caffeine.cache.simulator.policy.sketch.climbing.HillClimber.QueueType; 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 Window TinyLfu algorithm where the size of the admission window is adjusted using the a hill * climbing algorithm. * * @author ben.manes@gmail.com (Ben Manes) */ @SuppressWarnings("PMD.TooManyFields") public final class HillClimberWindowTinyLfuPolicy implements Policy { private final Long2ObjectMap<Node> data; private final PolicyStats policyStats; private final HillClimber climber; private final Admittor admittor; private final int maximumSize; private final Node headWindow; private final Node headProbation; private final Node headProtected; private int maxWindow; private int maxProtected; private int sizeWindow; private int sizeProtected; static final boolean debug = false; static final boolean trace = false; public HillClimberWindowTinyLfuPolicy(HillClimberType strategy, double percentMain, HillClimberWindowTinyLfuSettings settings) { String name = String.format("sketch.HillClimberWindowTinyLfu (%s %.0f%%)", strategy.name().toLowerCase(), 100 * (1.0 - percentMain)); this.policyStats = new PolicyStats(name); this.admittor = new TinyLfu(settings.config(), policyStats); this.climber = strategy.create(settings.config()); int maxMain = (int) (settings.maximumSize() * percentMain); this.maxProtected = (int) (maxMain * settings.percentMainProtected()); this.maxWindow = settings.maximumSize() - maxMain; this.data = new Long2ObjectOpenHashMap<>(); this.maximumSize = settings.maximumSize(); this.headProtected = new Node(); this.headProbation = new Node(); this.headWindow = new Node(); printSegmentSizes(); } /** Returns all variations of this policy based on the configuration parameters. */ public static Set<Policy> policies(Config config) { HillClimberWindowTinyLfuSettings settings = new HillClimberWindowTinyLfuSettings(config); Set<Policy> policies = new HashSet<>(); for (HillClimberType climber : settings.strategy()) { for (double percentMain : settings.percentMain()) { policies.add(new HillClimberWindowTinyLfuPolicy(climber, percentMain, settings)); } } return policies; } @Override public PolicyStats stats() { return policyStats; } @Override public void record(long key) { policyStats.recordOperation(); Node node = data.get(key); admittor.record(key); QueueType queue = null; if (node == null) { onMiss(key); policyStats.recordMiss(); } else { queue = node.queue; if (queue == WINDOW) { onWindowHit(node); policyStats.recordHit(); } else if (queue == PROBATION) { onProbationHit(node); policyStats.recordHit(); } else if (queue == PROTECTED) { onProtectedHit(node); policyStats.recordHit(); } else { throw new IllegalStateException(); } } climb(key, queue); } /** Adds the entry to the admission window, evicting if necessary. */ private void onMiss(long key) { Node node = new Node(key, WINDOW); node.appendToTail(headWindow); data.put(key, node); sizeWindow++; evict(); } /** Moves the entry to the MRU position in the admission window. */ private void onWindowHit(Node node) { node.moveToTail(headWindow); } /** Promotes the entry to the protected region's MRU position, demoting an entry if necessary. */ private void onProbationHit(Node node) { node.remove(); node.queue = PROTECTED; node.appendToTail(headProtected); sizeProtected++; demoteProtected(); } private void demoteProtected() { if (sizeProtected > maxProtected) { Node demote = headProtected.next; demote.remove(); demote.queue = PROBATION; demote.appendToTail(headProbation); sizeProtected--; } } /** Moves the entry to the MRU position, if it falls outside of the fast-path threshold. */ private void onProtectedHit(Node node) { node.moveToTail(headProtected); } /** * Evicts from the admission window into the probation space. If the size exceeds the maximum, * then the admission candidate and probation's victim are evaluated and one is evicted. */ private void evict() { if (sizeWindow <= maxWindow) { return; } Node candidate = headWindow.next; sizeWindow--; candidate.remove(); candidate.queue = PROBATION; candidate.appendToTail(headProbation); if (data.size() > maximumSize) { Node victim = headProbation.next; Node evict = admittor.admit(candidate.key, victim.key) ? victim : candidate; data.remove(evict.key); evict.remove(); policyStats.recordEviction(); } } /** Performs the hill climbing process. */ private void climb(long key, @Nullable QueueType queue) { if (data.size() < maximumSize) { return; } else if (queue == null) { climber.onMiss(key); } else { climber.onHit(key, queue); } Adaptation adaptation = climber.adapt(sizeWindow, sizeProtected); if (adaptation.type == INCREASE_WINDOW) { increaseWindow(adaptation.amount); } else if (adaptation.type == DECREASE_WINDOW) { decreaseWindow(adaptation.amount); } } private void increaseWindow(int amount) { if (maxProtected == 0) { return; } int steps = Math.min(amount, maxProtected); for (int i = 0; i < steps; i++) { maxWindow++; sizeWindow++; maxProtected--; demoteProtected(); Node candidate = headProbation.next; candidate.remove(); candidate.queue = WINDOW; candidate.appendToTail(headWindow); } if (trace) { System.out.printf("+%,d (%,d -> %,d)%n", steps, maxWindow - steps, maxWindow); } } private void decreaseWindow(int amount) { if (maxWindow == 0) { return; } int steps = Math.min(amount, maxWindow); for (int i = 0; i < steps; i++) { if (amount > 0) { maxWindow--; sizeWindow--; maxProtected++; Node candidate = headWindow.next; candidate.remove(); candidate.queue = PROBATION; candidate.appendToHead(headProbation); } } if (trace) { System.out.printf("-%,d (%,d -> %,d)%n", steps, maxWindow + steps, maxWindow); } } private void printSegmentSizes() { if (debug) { System.out.printf("maxWindow=%d, maxProtected=%d, percentWindow=%.1f", maxWindow, maxProtected, (100.0 * maxWindow) / maximumSize); } } @Override public void finished() { printSegmentSizes(); long windowSize = data.values().stream().filter(n -> n.queue == WINDOW).count(); long probationSize = data.values().stream().filter(n -> n.queue == PROBATION).count(); long protectedSize = data.values().stream().filter(n -> n.queue == PROTECTED).count(); checkState(windowSize == sizeWindow); checkState(protectedSize == sizeProtected); checkState(probationSize == data.size() - windowSize - protectedSize); checkState(data.size() <= maximumSize); } /** A node on the double-linked list. */ static final class Node { final long key; QueueType queue; Node prev; Node next; /** Creates a new sentinel node. */ public Node() { this.key = Integer.MIN_VALUE; this.prev = this; this.next = this; } /** Creates a new, unlinked node. */ public Node(long key, QueueType queue) { this.queue = queue; this.key = key; } public void moveToTail(Node head) { remove(); appendToTail(head); } /** Appends the node to the tail of the list. */ public void appendToHead(Node head) { Node first = head.next; head.next = this; first.prev = this; prev = head; next = first; } /** 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() { prev.next = next; next.prev = prev; next = prev = null; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("key", key) .add("queue", queue) .toString(); } } static final class HillClimberWindowTinyLfuSettings extends BasicSettings { public HillClimberWindowTinyLfuSettings(Config config) { super(config); } public List<Double> percentMain() { return config().getDoubleList("hill-climber-window-tiny-lfu.percent-main"); } public double percentMainProtected() { return config().getDouble("hill-climber-window-tiny-lfu.percent-main-protected"); } public Set<HillClimberType> strategy() { return config().getStringList("hill-climber-window-tiny-lfu.strategy").stream() .map(strategy -> strategy.replace('-', '_').toUpperCase()) .map(HillClimberType::valueOf) .collect(toSet()); } } }