HotEntryCache will update Instants only once per second
Calling Instant.now() several hundred thousand times per second can be expensive. In my measurements >10% of the time spend when loading new data was spend calling Instant.now(). Fixed this by storing an Instant as static member and updating it periodically in a separate thread.
This commit is contained in:
@@ -152,7 +152,8 @@ public class DataStore implements AutoCloseable {
|
|||||||
|
|
||||||
// A Doc will never be changed once it is created. Therefore we can cache them
|
// A Doc will never be changed once it is created. Therefore we can cache them
|
||||||
// easily.
|
// easily.
|
||||||
private final HotEntryCache<Long, Doc> docIdToDocCache = new HotEntryCache<>(Duration.ofMinutes(10));
|
private final HotEntryCache<Long, Doc> docIdToDocCache = new HotEntryCache<>(Duration.ofMinutes(10),
|
||||||
|
"docIdToDocCache");
|
||||||
|
|
||||||
private final DiskStorage diskStorage;
|
private final DiskStorage diskStorage;
|
||||||
private final Path diskStorageFilePath;
|
private final Path diskStorageFilePath;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import java.time.Clock;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.ConcurrentModificationException;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -97,9 +99,9 @@ public class HotEntryCache<K, V> {
|
|||||||
|
|
||||||
private V value;
|
private V value;
|
||||||
|
|
||||||
public Entry(final V value, final Clock clock) {
|
public Entry(final V value, final Instant creationTime) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
lastAccessed = Instant.now(clock);
|
lastAccessed = creationTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public V getValue() {
|
public V getValue() {
|
||||||
@@ -119,6 +121,39 @@ public class HotEntryCache<K, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class TimeUpdaterThread extends Thread {
|
||||||
|
|
||||||
|
private final WeakHashMap<HotEntryCache<?, ?>, Void> weakCaches = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public TimeUpdaterThread() {
|
||||||
|
setDaemon(true);
|
||||||
|
setName("HotEntryCache-time");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCache(final HotEntryCache<?, ?> cache) {
|
||||||
|
weakCaches.put(cache, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(1);
|
||||||
|
} catch (final InterruptedException e) {
|
||||||
|
// interrupted: update the 'now' instants of all caches
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (final HotEntryCache<?, ?> cache : weakCaches.keySet()) {
|
||||||
|
cache.updateTime();
|
||||||
|
}
|
||||||
|
} catch (final ConcurrentModificationException e) {
|
||||||
|
// ignore: might happen if an entry in weakCaches is garbage collected
|
||||||
|
// while we are iterating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class EvictionThread extends Thread {
|
private static final class EvictionThread extends Thread {
|
||||||
|
|
||||||
private static final Duration MAX_SLEEP_PERIOD = Duration.ofDays(1);
|
private static final Duration MAX_SLEEP_PERIOD = Duration.ofDays(1);
|
||||||
@@ -144,6 +179,7 @@ public class HotEntryCache<K, V> {
|
|||||||
|
|
||||||
final CompletableFuture<Void> future = this.future.getAcquire();
|
final CompletableFuture<Void> future = this.future.getAcquire();
|
||||||
|
|
||||||
|
try {
|
||||||
final Instant minNextEvictionTime = evictStaleEntries();
|
final Instant minNextEvictionTime = evictStaleEntries();
|
||||||
|
|
||||||
timeToNextEviction = normalizeDurationToNextEviction(minNextEvictionTime);
|
timeToNextEviction = normalizeDurationToNextEviction(minNextEvictionTime);
|
||||||
@@ -152,6 +188,10 @@ public class HotEntryCache<K, V> {
|
|||||||
future.complete(null);
|
future.complete(null);
|
||||||
this.future.set(null);
|
this.future.set(null);
|
||||||
}
|
}
|
||||||
|
} catch (final ConcurrentModificationException e) {
|
||||||
|
// ignore: might happen if an entry in weakCaches is garbage collected
|
||||||
|
// while we are iterating
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,10 +248,15 @@ public class HotEntryCache<K, V> {
|
|||||||
|
|
||||||
private static final EvictionThread EVICTER = new EvictionThread();
|
private static final EvictionThread EVICTER = new EvictionThread();
|
||||||
|
|
||||||
|
private static final TimeUpdaterThread TIME_UPDATER = new TimeUpdaterThread();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
EVICTER.start();
|
EVICTER.start();
|
||||||
|
TIME_UPDATER.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Instant now;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of the key to the value.
|
* Mapping of the key to the value.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -225,32 +270,44 @@ public class HotEntryCache<K, V> {
|
|||||||
|
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
|
|
||||||
HotEntryCache(final Duration timeToLive, final Clock clock) {
|
private final String name;
|
||||||
|
|
||||||
|
HotEntryCache(final Duration timeToLive, final Clock clock, final String name) {
|
||||||
this.timeToLive = timeToLive;
|
this.timeToLive = timeToLive;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
|
this.name = name;
|
||||||
|
now = Instant.now(clock);
|
||||||
|
|
||||||
EVICTER.addCache(this);
|
EVICTER.addCache(this);
|
||||||
|
TIME_UPDATER.addCache(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
HotEntryCache(final Duration timeToLive, final Clock clock) {
|
||||||
|
this(timeToLive, clock, UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HotEntryCache(final Duration timeToLive, final String name) {
|
||||||
|
this(timeToLive, Clock.systemDefaultZone(), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HotEntryCache(final Duration timeToLive) {
|
public HotEntryCache(final Duration timeToLive) {
|
||||||
this(timeToLive, Clock.systemDefaultZone());
|
this(timeToLive, Clock.systemDefaultZone(), UUID.randomUUID().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public int size() {
|
public int size() {
|
||||||
return cache.size();
|
return cache.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
public void addListener(final EventListener<K, V> listener, final EventType... eventTypes) {
|
public void addListener(final EventListener<K, V> listener, final EventType... eventTypes) {
|
||||||
listeners.add(new EventSubscribers<>(EnumSet.copyOf(Arrays.asList(eventTypes)), listener));
|
listeners.add(new EventSubscribers<>(EnumSet.copyOf(Arrays.asList(eventTypes)), listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
public V get(final K key) {
|
public V get(final K key) {
|
||||||
final Entry<V> entry = cache.computeIfPresent(key, (k, e) -> {
|
final Entry<V> entry = cache.computeIfPresent(key, (k, e) -> {
|
||||||
final Instant now = Instant.now(clock);
|
|
||||||
if (isExpired(e, now)) {
|
|
||||||
handleEvent(EventType.EVICTED, k, e.getValue());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
touch(key, e);
|
touch(key, e);
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
@@ -270,7 +327,8 @@ public class HotEntryCache<K, V> {
|
|||||||
oldEntry.setValue(value);
|
oldEntry.setValue(value);
|
||||||
entry = oldEntry;
|
entry = oldEntry;
|
||||||
} else {
|
} else {
|
||||||
entry = new Entry<>(value, clock);
|
final Instant creationTime = now();
|
||||||
|
entry = new Entry<>(value, creationTime);
|
||||||
}
|
}
|
||||||
touch(k, entry);
|
touch(k, entry);
|
||||||
return entry;
|
return entry;
|
||||||
@@ -302,7 +360,8 @@ public class HotEntryCache<K, V> {
|
|||||||
final boolean wasEmptyBefore = cache.isEmpty();
|
final boolean wasEmptyBefore = cache.isEmpty();
|
||||||
final Entry<V> entry = cache.computeIfAbsent(key, (k) -> {
|
final Entry<V> entry = cache.computeIfAbsent(key, (k) -> {
|
||||||
final V value = mappingFunction.apply(k);
|
final V value = mappingFunction.apply(k);
|
||||||
final Entry<V> e = new Entry<>(value, clock);
|
final Instant creationTime = now();
|
||||||
|
final Entry<V> e = new Entry<>(value, creationTime);
|
||||||
touch(key, e);
|
touch(key, e);
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
@@ -341,10 +400,10 @@ public class HotEntryCache<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Instant evict() {
|
private Instant evict() {
|
||||||
final Instant now = Instant.now(clock);
|
final Instant now = now();
|
||||||
final Instant oldestValuesToKeep = now.minus(timeToLive);
|
final Instant oldestValuesToKeep = now.minus(timeToLive);
|
||||||
Instant lastAccessTime = Instant.MAX;
|
Instant lastAccessTime = Instant.MAX;
|
||||||
LOGGER.trace("cache size before eviction {}", cache.size());
|
LOGGER.trace("{}: cache size before eviction {}", name, cache.size());
|
||||||
|
|
||||||
// for (final java.util.Map.Entry<Instant, Set<K>> mapEntry :
|
// for (final java.util.Map.Entry<Instant, Set<K>> mapEntry :
|
||||||
// lastAccessMap.entrySet()) {
|
// lastAccessMap.entrySet()) {
|
||||||
@@ -368,7 +427,7 @@ public class HotEntryCache<K, V> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
LOGGER.trace("cache size after eviction {}", cache.size());
|
LOGGER.trace("{}: cache size after eviction {}", name, cache.size());
|
||||||
|
|
||||||
final Instant nextEvictionTime = lastAccessTime.equals(Instant.MAX) ? Instant.MAX
|
final Instant nextEvictionTime = lastAccessTime.equals(Instant.MAX) ? Instant.MAX
|
||||||
: lastAccessTime.plus(timeToLive);
|
: lastAccessTime.plus(timeToLive);
|
||||||
@@ -383,11 +442,18 @@ public class HotEntryCache<K, V> {
|
|||||||
return a.compareTo(b) < 0 ? a : b;
|
return a.compareTo(b) < 0 ? a : b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Instant now() {
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for test
|
||||||
|
void updateTime() {
|
||||||
|
now = Instant.now(clock);
|
||||||
|
}
|
||||||
|
|
||||||
private void touch(final K key, final Entry<V> entry) {
|
private void touch(final K key, final Entry<V> entry) {
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
|
final Instant now = now();
|
||||||
final Instant now = Instant.now(clock);
|
|
||||||
|
|
||||||
entry.touch(now);
|
entry.touch(now);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -408,6 +474,7 @@ public class HotEntryCache<K, V> {
|
|||||||
|
|
||||||
// visible for test
|
// visible for test
|
||||||
void triggerEvictionAndWait() {
|
void triggerEvictionAndWait() {
|
||||||
|
updateTime();
|
||||||
final Future<Void> future = EVICTER.nextEvictionChangedWithFuture();
|
final Future<Void> future = EVICTER.nextEvictionChangedWithFuture();
|
||||||
try {
|
try {
|
||||||
future.get(5, TimeUnit.MINUTES);
|
future.get(5, TimeUnit.MINUTES);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class HotEntryCacheTest {
|
|||||||
cache.put("key", "value1");
|
cache.put("key", "value1");
|
||||||
|
|
||||||
clock.plusSeconds(2);
|
clock.plusSeconds(2);
|
||||||
|
cache.updateTime();
|
||||||
|
|
||||||
cache.put("key", "value2");
|
cache.put("key", "value2");
|
||||||
|
|
||||||
@@ -64,19 +65,23 @@ public class HotEntryCacheTest {
|
|||||||
Assert.assertEquals(cachedValue1_evicted, null);
|
Assert.assertEquals(cachedValue1_evicted, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO that does not make sense. Get should not evict. We should be happy that
|
public void testGetTouches() throws Exception {
|
||||||
// the element is still in the map when we need it.
|
|
||||||
public void testGetEvicts() throws Exception {
|
|
||||||
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
|
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
|
||||||
final Duration timeToLive = Duration.ofSeconds(10);
|
final Duration timeToLive = Duration.ofSeconds(10);
|
||||||
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
|
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
|
||||||
|
|
||||||
cache.put("key", "value1");
|
cache.put("key", "value1");
|
||||||
|
|
||||||
|
// skip forward in time, but do not yet trigger eviction
|
||||||
clock.plus(timeToLive.plusMillis(1));
|
clock.plus(timeToLive.plusMillis(1));
|
||||||
|
cache.updateTime();
|
||||||
|
|
||||||
final String cachedValue1_evicted = cache.get("key");
|
cache.get("key"); // will touch the entry
|
||||||
Assert.assertEquals(cachedValue1_evicted, null);
|
|
||||||
|
cache.triggerEvictionAndWait(); // if get didn't touch, then this will evict the entry
|
||||||
|
|
||||||
|
final String cachedValue1 = cache.get("key");
|
||||||
|
Assert.assertEquals(cachedValue1, "value1");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEvictionByBackgroundThread() throws InterruptedException, ExecutionException, TimeoutException {
|
public void testEvictionByBackgroundThread() throws InterruptedException, ExecutionException, TimeoutException {
|
||||||
@@ -92,6 +97,7 @@ public class HotEntryCacheTest {
|
|||||||
cache.put("key", "value1");
|
cache.put("key", "value1");
|
||||||
|
|
||||||
clock.plus(timeToLive.minusSeconds(1));
|
clock.plus(timeToLive.minusSeconds(1));
|
||||||
|
cache.updateTime();
|
||||||
|
|
||||||
cache.put("key2", "value2");
|
cache.put("key2", "value2");
|
||||||
clock.plus(Duration.ofSeconds(1).plusMillis(1));
|
clock.plus(Duration.ofSeconds(1).plusMillis(1));
|
||||||
@@ -149,6 +155,7 @@ public class HotEntryCacheTest {
|
|||||||
|
|
||||||
// seek, so that it is almost evicted
|
// seek, so that it is almost evicted
|
||||||
clock.plus(timeToLive.minusMillis(1));
|
clock.plus(timeToLive.minusMillis(1));
|
||||||
|
cache.updateTime();
|
||||||
|
|
||||||
// the for each should touch the entries
|
// the for each should touch the entries
|
||||||
cache.forEach(s -> {
|
cache.forEach(s -> {
|
||||||
@@ -156,12 +163,14 @@ public class HotEntryCacheTest {
|
|||||||
|
|
||||||
// seek again
|
// seek again
|
||||||
clock.plus(timeToLive.minusMillis(1));
|
clock.plus(timeToLive.minusMillis(1));
|
||||||
|
cache.triggerEvictionAndWait();
|
||||||
|
|
||||||
// if the touch didn't happen, then the value is now evicted
|
// if the touch didn't happen, then the value is now evicted
|
||||||
Assert.assertEquals(evictionEventFuture.isDone(), false);
|
Assert.assertEquals(evictionEventFuture.isDone(), false);
|
||||||
|
|
||||||
// seek again, so that the entry will get evicted
|
// seek again, so that the entry will get evicted
|
||||||
clock.plus(timeToLive.minusMillis(1));
|
clock.plus(timeToLive.minusMillis(1));
|
||||||
|
cache.triggerEvictionAndWait();
|
||||||
|
|
||||||
Assert.assertEquals(cache.get("key"), null);
|
Assert.assertEquals(cache.get("key"), null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class TagsToFile implements AutoCloseable {
|
|||||||
public TagsToFile(final DataStore dataStore) {
|
public TagsToFile(final DataStore dataStore) {
|
||||||
this.dataStore = dataStore;
|
this.dataStore = dataStore;
|
||||||
|
|
||||||
writerCache = new HotEntryCache<>(Duration.ofSeconds(10));
|
writerCache = new HotEntryCache<>(Duration.ofSeconds(10), "writerCache");
|
||||||
writerCache.addListener(new RemovalListener(), EventType.EVICTED, EventType.REMOVED);
|
writerCache.addListener(new RemovalListener(), EventType.EVICTED, EventType.REMOVED);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user