/*
* 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.sampled;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
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;
/**
* A cache that uses a sampled array of entries to implement simple page replacement algorithms.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class SampledPolicy implements Policy {
final Long2ObjectMap<Node> data;
final PolicyStats policyStats;
final EvictionPolicy policy;
final Sample sampleStrategy;
final Admittor admittor;
final int maximumSize;
final int sampleSize;
final Random random;
final Node[] table;
long tick;
public SampledPolicy(Admission admission, EvictionPolicy policy, Config config) {
this.policyStats = new PolicyStats(admission.format("sampled." + policy.label()));
this.admittor = admission.from(config, policyStats);
SampledSettings settings = new SampledSettings(config);
this.sampleStrategy = settings.sampleStrategy();
this.random = new Random(settings.randomSeed());
this.data = new Long2ObjectOpenHashMap<>();
this.maximumSize = settings.maximumSize();
this.sampleSize = settings.sampleSize();
this.table = new Node[maximumSize + 1];
this.policy = policy;
}
/** 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 SampledPolicy(admission, policy, config)
).collect(toSet());
}
@Override
public PolicyStats stats() {
return policyStats;
}
@Override
public void record(long key) {
Node node = data.get(key);
admittor.record(key);
long now = ++tick;
if (node == null) {
node = new Node(key, data.size(), now);
policyStats.recordOperation();
policyStats.recordMiss();
table[node.index] = node;
data.put(key, node);
evict(node);
} else {
policyStats.recordOperation();
policyStats.recordHit();
node.accessTime = now;
node.frequency++;
}
}
/** Evicts if the map exceeds the maximum capacity. */
private void evict(Node candidate) {
if (data.size() > maximumSize) {
List<Node> sample = (policy == EvictionPolicy.RANDOM)
? Arrays.asList(table)
: sampleStrategy.sample(table, candidate, sampleSize, random, policyStats);
Node victim = policy.select(sample, random);
policyStats.recordEviction();
if (admittor.admit(candidate.key, victim.key)) {
removeFromTable(victim);
data.remove(victim.key);
} else {
removeFromTable(candidate);
data.remove(candidate.key);
}
}
}
/** Removes the node from the table and adds the index to the free list. */
private void removeFromTable(Node node) {
int last = data.size() - 1;
table[node.index] = table[last];
table[node.index].index = node.index;
table[last] = null;
}
/** The algorithms to choose a random sample with. */
public enum Sample {
GUESS {
@Override public <E> List<E> sample(E[] elements, E candidate,
int sampleSize, Random random, PolicyStats policyStats) {
List<E> sample = new ArrayList<>(sampleSize);
policyStats.addOperations(sampleSize);
for (int i = 0; i < sampleSize; i++) {
int index = random.nextInt(elements.length);
if (elements[index] == candidate) {
i--; // try again
}
sample.add(elements[index]);
}
return sample;
}
},
RESERVOIR {
@Override public <E> List<E> sample(E[] elements, E candidate,
int sampleSize, Random random, PolicyStats policyStats) {
List<E> sample = new ArrayList<>(sampleSize);
policyStats.addOperations(elements.length);
int count = 0;
for (E e : elements) {
if (e == candidate) {
continue;
}
count++;
if (sample.size() <= sampleSize) {
sample.add(e);
} else {
int index = random.nextInt(count);
if (index < sampleSize) {
sample.set(index, e);
}
}
}
return sample;
}
},
SHUFFLE {
@Override public <E> List<E> sample(E[] elements, E candidate,
int sampleSize, Random random, PolicyStats policyStats) {
List<E> sample = new ArrayList<>(Arrays.asList(elements));
policyStats.addOperations(elements.length);
Collections.shuffle(sample, random);
sample.remove(candidate);
return sample.subList(0, sampleSize);
}
};
abstract <E> List<E> sample(E[] elements, E candidate,
int sampleSize, Random random, PolicyStats policyStats);
}
/** The replacement policy. */
public enum EvictionPolicy {
/** Evicts entries based on insertion order. */
FIFO {
@Override Node select(List<Node> sample, Random random) {
return sample.stream().min((first, second) ->
Long.compare(first.insertionTime, second.insertionTime)).get();
}
},
/** Evicts entries based on how recently they are used, with the least recent evicted first. */
LRU {
@Override Node select(List<Node> sample, Random random) {
return sample.stream().min((first, second) ->
Long.compare(first.accessTime, second.accessTime)).get();
}
},
/** Evicts entries based on how recently they are used, with the least recent evicted first. */
MRU {
@Override Node select(List<Node> sample, Random random) {
return sample.stream().max((first, second) ->
Long.compare(first.accessTime, second.accessTime)).get();
}
},
/**
* Evicts entries based on how frequently they are used, with the least frequent evicted first.
*/
LFU {
@Override Node select(List<Node> sample, Random random) {
return sample.stream().min((first, second) ->
Long.compare(first.frequency, second.frequency)).get();
}
},
/**
* Evicts entries based on how frequently they are used, with the most frequent evicted first.
*/
MFU {
@Override Node select(List<Node> sample, Random random) {
return sample.stream().max((first, second) ->
Long.compare(first.frequency, second.frequency)).get();
}
},
/** Evicts a random entry. */
RANDOM {
@Override Node select(List<Node> sample, Random random) {
int victim = random.nextInt(sample.size());
return sample.get(victim);
}
};
public String label() {
return StringUtils.capitalize(name().toLowerCase());
}
/** Determines which node to evict. */
abstract Node select(List<Node> sample, Random random);
}
/** A node on the double-linked list. */
static final class Node {
final long key;
final long insertionTime;
long accessTime;
int frequency;
int index;
/** Creates a new node. */
public Node(long key, int index, long tick) {
this.insertionTime = tick;
this.accessTime = tick;
this.index = index;
this.key = key;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("key", key)
.add("index", index)
.toString();
}
}
static final class SampledSettings extends BasicSettings {
public SampledSettings(Config config) {
super(config);
}
public int sampleSize() {
return config().getInt("sampled.size");
}
public Sample sampleStrategy() {
return Sample.valueOf(config().getString("sampled.strategy").toUpperCase());
}
}
}