/* * 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 java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toSet; import java.util.Set; import org.apache.commons.lang3.StringUtils; import com.github.benmanes.caffeine.cache.simulator.BasicSettings; import com.github.benmanes.caffeine.cache.simulator.admission.Admission; import com.github.benmanes.caffeine.cache.simulator.admission.Admittor; 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.typesafe.config.Config; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; /** * Least/Most Frequency Used in O(1) time as described in <a href="http://dhruvbird.com/lfu.pdf"> An * O(1) algorithm for implementing the LFU cache eviction scheme</a>. * * @author ben.manes@gmail.com (Ben Manes) */ public final class FrequentlyUsedPolicy implements Policy { final PolicyStats policyStats; final Long2ObjectMap<Node> data; final EvictionPolicy policy; final FrequencyNode freq0; final Admittor admittor; final int maximumSize; public FrequentlyUsedPolicy(Admission admission, EvictionPolicy policy, Config config) { this.policyStats = new PolicyStats(admission.format("linked." + policy.label())); this.admittor = admission.from(config, policyStats); BasicSettings settings = new BasicSettings(config); this.data = new Long2ObjectOpenHashMap<>(); this.maximumSize = settings.maximumSize(); this.policy = requireNonNull(policy); this.freq0 = new FrequencyNode(); } /** Returns all variations of this policy based on the configuration parameters. */ public static Set<Policy> policies(Config config, EvictionPolicy policy) { BasicSettings settings = new BasicSettings(config); return settings.admission().stream().map(admission -> new FrequentlyUsedPolicy(admission, policy, config) ).collect(toSet()); } @Override public PolicyStats stats() { return policyStats; } @Override public void record(long key) { policyStats.recordOperation(); Node node = data.get(key); admittor.record(key); if (node == null) { onMiss(key); } else { onHit(node); } } /** Moves the entry to the next higher frequency list, creating it if necessary. */ private void onHit(Node node) { policyStats.recordHit(); int newCount = node.freq.count + 1; FrequencyNode freqN = (node.freq.next.count == newCount) ? node.freq.next : new FrequencyNode(newCount, node.freq); node.remove(); if (node.freq.isEmpty()) { node.freq.remove(); } node.freq = freqN; node.append(); } /** Adds the entry, creating an initial frequency list of 1 if necessary, and evicts if needed. */ private void onMiss(long key) { FrequencyNode freq1 = (freq0.next.count == 1) ? freq0.next : new FrequencyNode(1, freq0); Node node = new Node(key, freq1); policyStats.recordMiss(); data.put(key, node); node.append(); evict(node); } /** Evicts while the map exceeds the maximum capacity. */ private void evict(Node candidate) { if (data.size() > maximumSize) { Node victim = nextVictim(candidate); boolean admit = admittor.admit(candidate.key, victim.key); if (admit) { evictEntry(victim); } else { evictEntry(candidate); } policyStats.recordEviction(); } } /** * Returns the next victim, excluding the newly added candidate. This exclusion is required so * that a candidate has a fair chance to be used, rather than always rejected due to existing * entries having a high frequency from the distant past. */ Node nextVictim(Node candidate) { if (policy == EvictionPolicy.MFU) { // highest, never the candidate return freq0.prev.nextNode.next; } // find the lowest that is not the candidate Node victim = freq0.next.nextNode.next; if (victim == candidate) { victim = (victim.next == victim.prev) ? victim.freq.next.nextNode.next : victim.next; } return victim; } /** Removes the entry. */ private void evictEntry(Node node) { data.remove(node.key); node.remove(); if (node.freq.isEmpty()) { node.freq.remove(); } } public enum EvictionPolicy { LFU, MFU; public String label() { return StringUtils.capitalize(name().toLowerCase()); } } /** A frequency count and associated chain of cache entries. */ static final class FrequencyNode { final int count; final Node nextNode; FrequencyNode prev; FrequencyNode next; public FrequencyNode() { nextNode = new Node(this); this.prev = this; this.next = this; this.count = 0; } public FrequencyNode(int count, FrequencyNode prev) { nextNode = new Node(this); this.prev = prev; this.next = prev.next; prev.next = this; next.prev = this; this.count = count; } public boolean isEmpty() { return (nextNode == nextNode.next); } /** 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("count", count) .toString(); } } /** A cache entry on the frequency node's chain. */ static final class Node { final long key; FrequencyNode freq; Node prev; Node next; public Node(FrequencyNode freq) { this.key = Long.MIN_VALUE; this.freq = freq; this.prev = this; this.next = this; } public Node(long key, FrequencyNode freq) { this.next = null; this.prev = null; this.freq = freq; this.key = key; } /** Appends the node to the tail of the list. */ public void append() { prev = freq.nextNode.prev; next = freq.nextNode; prev.next = this; next.prev = this; } /** 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("freq", freq) .toString(); } } }