/* * 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.feedback; 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.admission.TinyLfu; import com.github.benmanes.caffeine.cache.simulator.membership.Membership; 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; /** * The TinyLfu + Lru algorithm where arrival is given more emphasis based on success of previous * predictions. If a candidate is rejected multiple times within a sample period then a new arrival * is given a higher frequency gain. The gain is decreased on an eviction if no adjustments have * been made recently. This allows the policy to dynamically decide whether it should favor recency * or frequency based on the workload's characteristics. If the workload changes then the policy * will adapt to the new environment. * * @author ben.manes@gmail.com (Ben Manes) */ public final class FeedbackTinyLfuPolicy implements Policy { private final Long2ObjectMap<Node> data; private final PolicyStats policyStats; private final TinyLfu admittor; private final int maximumSize; private final Node head; private int gain; private final int maxGain; private int sample; private int sampled; private int adjusted; private final int sampleSize; private final Membership feedback; boolean debug; public FeedbackTinyLfuPolicy(Config config) { FeedbackTinyLfuSettings settings = new FeedbackTinyLfuSettings(config); this.policyStats = new PolicyStats("sketch.FeedbackTinyLfu"); this.admittor = new TinyLfu(settings.config(), policyStats); this.data = new Long2ObjectOpenHashMap<>(); this.maximumSize = settings.maximumSize(); this.head = new Node(); maxGain = Math.min(15, settings.maximumInsertionGain()); sampleSize = Math.min(settings.maximumSampleSize(), maximumSize); feedback = settings.membershipFilter().create(sampleSize, settings.adaptiveFpp(), config); } /** Returns all variations of this policy based on the configuration parameters. */ public static Set<Policy> policies(Config config) { return ImmutableSet.of(new FeedbackTinyLfuPolicy(config)); } @Override public PolicyStats stats() { return policyStats; } @Override public void record(long key) { if ((sample % sampleSize) == 0) { sampled++; } if (sample % (sampleSize / 2) == 0) { feedback.clear(); } sample++; admittor.record(key); policyStats.recordOperation(); Node node = data.get(key); if (node == null) { onMiss(key); policyStats.recordMiss(); } else { onHit(node); policyStats.recordHit(); } } /** Adds the entry, evicting if necessary. */ private void onMiss(long key) { for (int i = 0; i < gain; i++) { admittor.record(key); } Node node = new Node(key); node.appendToTail(head); data.put(key, node); evict(node); } /** Moves the entry to the MRU position. */ private void onHit(Node node) { node.moveToTail(head); } /** * If the size exceeds the maximum, then the candidate and victim are evaluated and one is * evicted. */ private void evict(Node candidate) { if (data.size() > maximumSize) { Node evict; Node victim = head.next; if (admittor.admit(candidate.key, victim.key)) { evict = victim; } else if (adapt(candidate)) { evict = victim; } else { evict = candidate; feedback.put(candidate.key); } data.remove(evict.key); evict.remove(); policyStats.recordEviction(); } } private boolean adapt(Node candidate) { if (adjusted == sampled) { // Already adjusted this period return false; } if (feedback.mightContain(candidate.key)) { if (sampled >= (adjusted + gain)) { adjusted = sampled; // Increase arrival emphasis if (gain < maxGain) { gain++; } } return true; } else if (sampled > (adjusted + gain + 1)) { adjusted = sampled; // Decrease arrival emphasis if (gain > 0) { gain--; } } return false; } @Override public void finished() { if (debug) { System.out.println("recency gain = " + gain); } checkState(data.size() <= maximumSize, data.size()); } /** A node on the double-linked list. */ static final class Node { final long key; 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) { this.key = key; } public void moveToTail(Node head) { remove(); appendToTail(head); } /** 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) .toString(); } } static final class FeedbackTinyLfuSettings extends BasicSettings { public FeedbackTinyLfuSettings(Config config) { super(config); } public int maximumInsertionGain() { return config().getInt("feedback-tiny-lfu.maximum-insertion-gain"); } public int maximumSampleSize() { return config().getInt("feedback-tiny-lfu.maximum-sample-size"); } public double adaptiveFpp() { return config().getDouble("feedback-tiny-lfu.adaptive-fpp"); } } }