| /******************************************************************************* |
| * Copyright (c) 2000, 2013 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.core.util; |
| |
| import java.text.NumberFormat; |
| import java.util.Enumeration; |
| import java.util.Hashtable; |
| import java.util.List; |
| |
| import org.eclipse.jdt.internal.core.JavaElement; |
| import org.eclipse.jdt.internal.core.util.ToStringSorter.Pair; |
| |
| /** |
| * The <code>LRUCache</code> is a hashtable that stores a finite number of elements. |
| * When an attempt is made to add values to a full cache, the least recently used values |
| * in the cache are discarded to make room for the new values as necessary. |
| * |
| * <p>The data structure is based on the LRU virtual memory paging scheme. |
| * |
| * <p>Objects can take up a variable amount of cache space by implementing |
| * the <code>ILRUCacheable</code> interface. |
| * |
| * <p>This implementation is NOT thread-safe. Synchronization wrappers would |
| * have to be added to ensure atomic insertions and deletions from the cache. |
| * |
| * @see org.eclipse.jdt.internal.core.util.ILRUCacheable |
| */ |
| public class LRUCache<K, V> implements Cloneable { |
| |
| /** |
| * This type is used internally by the LRUCache to represent entries |
| * stored in the cache. |
| * It is static because it does not require a pointer to the cache |
| * which contains it. |
| * |
| * @see LRUCache |
| */ |
| public static class LRUCacheEntry<K, V> { |
| |
| /** |
| * Hash table key |
| */ |
| public K key; |
| |
| /** |
| * Hash table value (an LRUCacheEntry object) |
| */ |
| public V value; |
| |
| /** |
| * Time value for queue sorting |
| */ |
| public int timestamp; |
| |
| /** |
| * Cache footprint of this entry |
| */ |
| public int space; |
| |
| /** |
| * Previous entry in queue |
| */ |
| public LRUCacheEntry<K, V> previous; |
| |
| /** |
| * Next entry in queue |
| */ |
| public LRUCacheEntry<K, V> next; |
| |
| /** |
| * Creates a new instance of the receiver with the provided values |
| * for key, value, and space. |
| */ |
| public LRUCacheEntry (K key, V value, int space) { |
| this.key = key; |
| this.value = value; |
| this.space = space; |
| } |
| |
| /** |
| * Returns a String that represents the value of this object. |
| */ |
| @Override |
| public String toString() { |
| return "LRUCacheEntry [" + this.key + "-->" + this.value + "]"; //$NON-NLS-3$ //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| |
| public class Stats { |
| private int[] counters = new int[20]; |
| private long[] timestamps = new long[20]; |
| private int counterIndex = -1; |
| |
| private void add(int counter) { |
| for (int i = 0; i <= this.counterIndex; i++) { |
| if (this.counters[i] == counter) |
| return; |
| } |
| int length = this.counters.length; |
| if (++this.counterIndex == length) { |
| int newLength = this.counters.length * 2; |
| System.arraycopy(this.counters, 0, this.counters = new int[newLength], 0, length); |
| System.arraycopy(this.timestamps, 0, this.timestamps = new long[newLength], 0, length); |
| } |
| this.counters[this.counterIndex] = counter; |
| this.timestamps[this.counterIndex] = System.currentTimeMillis(); |
| } |
| |
| private String getAverageAge(long totalTime, int numberOfElements, long currentTime) { |
| if (numberOfElements == 0) |
| return "N/A"; //$NON-NLS-1$ |
| long time = totalTime / numberOfElements; |
| long age = currentTime - time; |
| long ageInSeconds = age/1000; |
| int seconds = 0; |
| int minutes = 0; |
| int hours = 0; |
| int days = 0; |
| if (ageInSeconds > 60) { |
| long ageInMin = ageInSeconds / 60; |
| seconds = (int) (ageInSeconds - (60 * ageInMin)); |
| if (ageInMin > 60) { |
| long ageInHours = ageInMin / 60; |
| minutes = (int) (ageInMin - (60 * ageInHours)); |
| if (ageInHours > 24) { |
| long ageInDays = ageInHours / 24; |
| hours = (int) (ageInHours - (24 * ageInDays)); |
| days = (int) ageInDays; |
| } else { |
| hours = (int) ageInHours; |
| } |
| } else { |
| minutes = (int) ageInMin; |
| } |
| } else { |
| seconds = (int) ageInSeconds; |
| } |
| StringBuffer buffer = new StringBuffer(); |
| if (days > 0) { |
| buffer.append(days); |
| buffer.append(" days "); //$NON-NLS-1$ |
| } |
| if (hours > 0) { |
| buffer.append(hours); |
| buffer.append(" hours "); //$NON-NLS-1$ |
| } |
| if (minutes > 0) { |
| buffer.append(minutes); |
| buffer.append(" minutes "); //$NON-NLS-1$ |
| } |
| buffer.append(seconds); |
| buffer.append(" seconds"); //$NON-NLS-1$ |
| return buffer.toString(); |
| } |
| |
| private long getTimestamps(int counter) { |
| for (int i = 0; i <= this.counterIndex; i++) { |
| if (this.counters[i] >= counter) |
| return this.timestamps[i]; |
| } |
| return -1; |
| } |
| |
| public synchronized String printStats() { |
| int numberOfElements = LRUCache.this.currentSpace; |
| if (numberOfElements == 0) { |
| return "No elements in cache"; //$NON-NLS-1$ |
| } |
| StringBuffer buffer = new StringBuffer(); |
| |
| buffer.append("Number of elements in cache: "); //$NON-NLS-1$ |
| buffer.append(numberOfElements); |
| |
| final int numberOfGroups = 5; |
| int numberOfElementsPerGroup = numberOfElements / numberOfGroups; |
| buffer.append("\n("); //$NON-NLS-1$ |
| buffer.append(numberOfGroups); |
| buffer.append(" groups of "); //$NON-NLS-1$ |
| buffer.append(numberOfElementsPerGroup); |
| buffer.append(" elements)"); //$NON-NLS-1$ |
| buffer.append("\n\nAverage age:"); //$NON-NLS-1$ |
| int groupNumber = 1; |
| int elementCounter = 0; |
| LRUCacheEntry<K, V> entry = LRUCache.this.entryQueueTail; |
| long currentTime = System.currentTimeMillis(); |
| long accumulatedTime = 0; |
| while (entry != null) { |
| long timeStamps = getTimestamps(entry.timestamp); |
| if (timeStamps > 0) { |
| accumulatedTime += timeStamps; |
| elementCounter++; |
| } |
| if (elementCounter >= numberOfElementsPerGroup && (groupNumber < numberOfGroups)) { |
| buffer.append("\nGroup "); //$NON-NLS-1$ |
| buffer.append(groupNumber); |
| if (groupNumber == 1) { |
| buffer.append(" (oldest)\t: "); //$NON-NLS-1$ |
| } else { |
| buffer.append("\t\t: "); //$NON-NLS-1$ |
| } |
| groupNumber++; |
| buffer.append(getAverageAge(accumulatedTime, elementCounter, currentTime)); |
| elementCounter = 0; |
| accumulatedTime = 0; |
| } |
| entry = entry.previous; |
| } |
| buffer.append("\nGroup "); //$NON-NLS-1$ |
| buffer.append(numberOfGroups); |
| buffer.append(" (youngest)\t: "); //$NON-NLS-1$ |
| buffer.append(getAverageAge(accumulatedTime, elementCounter, currentTime)); |
| |
| return buffer.toString(); |
| } |
| |
| private void removeCountersOlderThan(int counter) { |
| for (int i = 0; i <= this.counterIndex; i++) { |
| if (this.counters[i] >= counter) { |
| if (i > 0) { |
| int length = this.counterIndex-i+1; |
| System.arraycopy(this.counters, i, this.counters, 0, length); |
| System.arraycopy(this.timestamps, i, this.timestamps, 0, length); |
| this.counterIndex = length; |
| } |
| return; |
| } |
| } |
| } |
| |
| public K getOldestElement() { |
| return LRUCache.this.getOldestElement(); |
| } |
| |
| public long getOldestTimestamps() { |
| return getTimestamps(getOldestTimestampCounter()); |
| } |
| |
| public synchronized void snapshot() { |
| removeCountersOlderThan(getOldestTimestampCounter()); |
| add(getNewestTimestampCounter()); |
| } |
| } |
| |
| /** |
| * Amount of cache space used so far |
| */ |
| protected int currentSpace; |
| |
| /** |
| * Maximum space allowed in cache |
| */ |
| protected int spaceLimit; |
| |
| /** |
| * Counter for handing out sequential timestamps |
| */ |
| protected int timestampCounter; |
| |
| /** |
| * Hash table for fast random access to cache entries |
| */ |
| protected Hashtable<K, LRUCacheEntry<K, V>> entryTable; |
| |
| /** |
| * Start of queue (most recently used entry) |
| */ |
| protected LRUCacheEntry<K, V> entryQueue; |
| |
| /** |
| * End of queue (least recently used entry) |
| */ |
| protected LRUCacheEntry<K, V> entryQueueTail; |
| |
| /** |
| * Default amount of space in the cache |
| */ |
| protected static final int DEFAULT_SPACELIMIT = 100; |
| |
| /** |
| * Creates a new cache. Size of cache is defined by |
| * <code>DEFAULT_SPACELIMIT</code>. |
| */ |
| public LRUCache() { |
| this(DEFAULT_SPACELIMIT); |
| } |
| |
| /** |
| * Creates a new cache. |
| * @param size Size of Cache |
| */ |
| public LRUCache(int size) { |
| this.timestampCounter = this.currentSpace = 0; |
| this.entryQueue = this.entryQueueTail = null; |
| this.entryTable = new Hashtable<>(size); |
| this.spaceLimit = size; |
| } |
| |
| /** |
| * Returns a new cache containing the same contents. |
| * |
| * @return New copy of object. |
| */ |
| @Override |
| public LRUCache<K, V> clone() { |
| LRUCache<K, V> newCache = newInstance(this.spaceLimit); |
| LRUCacheEntry<K, V> qEntry; |
| |
| /* Preserve order of entries by copying from oldest to newest */ |
| qEntry = this.entryQueueTail; |
| while (qEntry != null) { |
| newCache.privateAdd (qEntry.key, qEntry.value, qEntry.space); |
| qEntry = qEntry.previous; |
| } |
| return newCache; |
| } |
| |
| public double fillingRatio() { |
| return (this.currentSpace) * 100.0 / this.spaceLimit; |
| } |
| |
| /** |
| * Flushes all entries from the cache. |
| */ |
| public void flush() { |
| this.currentSpace = 0; |
| LRUCacheEntry<K, V> entry = this.entryQueueTail; // Remember last entry |
| this.entryTable = new Hashtable<>(); // Clear it out |
| this.entryQueue = this.entryQueueTail = null; |
| while (entry != null) { // send deletion notifications in LRU order |
| entry = entry.previous; |
| } |
| } |
| |
| /** |
| * Flushes the given entry from the cache. Does nothing if entry does not |
| * exist in cache. |
| * |
| * @param key Key of object to flush |
| */ |
| public void flush (K key) { |
| LRUCacheEntry<K, V> entry; |
| entry = this.entryTable.get(key); |
| |
| /* If entry does not exist, return */ |
| if (entry == null) return; |
| |
| privateRemoveEntry (entry, false); |
| } |
| |
| /* |
| * Answers the existing key that is equals to the given key. |
| * If the key is not in the cache, returns the given key |
| */ |
| public K getKey(K key) { |
| LRUCacheEntry<K, V> entry = this.entryTable.get(key); |
| if (entry == null) { |
| return key; |
| } |
| return entry.key; |
| } |
| |
| /** |
| * Answers the value in the cache at the given key. |
| * If the value is not in the cache, returns null |
| * |
| * @param key Hash table key of object to retrieve |
| * @return Retrieved object, or null if object does not exist |
| */ |
| public V get(K key) { |
| LRUCacheEntry<K, V> entry = this.entryTable.get(key); |
| if (entry == null) { |
| return null; |
| } |
| |
| updateTimestamp (entry); |
| return entry.value; |
| } |
| |
| /** |
| * Returns the amount of space that is current used in the cache. |
| */ |
| public int getCurrentSpace() { |
| return this.currentSpace; |
| } |
| |
| /** |
| * Returns the timestamps of the most recently used element in the cache. |
| */ |
| public int getNewestTimestampCounter() { |
| return this.entryQueue == null ? 0 : this.entryQueue.timestamp; |
| } |
| |
| /** |
| * Returns the timestamps of the least recently used element in the cache. |
| */ |
| public int getOldestTimestampCounter() { |
| return this.entryQueueTail == null ? 0 : this.entryQueueTail.timestamp; |
| } |
| |
| /** |
| * Returns the lest recently used element in the cache, can return {@code null} |
| */ |
| public K getOldestElement() { |
| return this.entryQueueTail == null ? null : this.entryQueueTail.key; |
| } |
| |
| /** |
| * Returns the maximum amount of space available in the cache. |
| */ |
| public int getSpaceLimit() { |
| return this.spaceLimit; |
| } |
| |
| /** |
| * Returns an Enumeration of the keys currently in the cache. |
| */ |
| public Enumeration<K> keys() { |
| return this.entryTable.keys(); |
| } |
| |
| /** |
| * Returns an enumeration that iterates over all the keys and values |
| * currently in the cache. |
| */ |
| public ICacheEnumeration<K, V> keysAndValues() { |
| return new ICacheEnumeration<K, V>() { |
| |
| Enumeration<LRUCacheEntry<K, V>> values = LRUCache.this.entryTable.elements(); |
| LRUCacheEntry<K, V> entry; |
| |
| @Override |
| public boolean hasMoreElements() { |
| return this.values.hasMoreElements(); |
| } |
| |
| @Override |
| public K nextElement() { |
| this.entry = this.values.nextElement(); |
| return this.entry.key; |
| } |
| |
| @Override |
| public V getValue() { |
| if (this.entry == null) { |
| throw new java.util.NoSuchElementException(); |
| } |
| return this.entry.value; |
| } |
| }; |
| } |
| |
| /** |
| * Ensures there is the specified amount of free space in the receiver, |
| * by removing old entries if necessary. Returns true if the requested space was |
| * made available, false otherwise. |
| * |
| * @param space Amount of space to free up |
| */ |
| protected boolean makeSpace (int space) { |
| |
| int limit; |
| |
| limit = getSpaceLimit(); |
| |
| /* if space is already available */ |
| if (this.currentSpace + space <= limit) { |
| return true; |
| } |
| |
| /* if entry is too big for cache */ |
| if (space > limit) { |
| return false; |
| } |
| |
| /* Free up space by removing oldest entries */ |
| while (this.currentSpace + space > limit && this.entryQueueTail != null) { |
| privateRemoveEntry (this.entryQueueTail, false); |
| } |
| return true; |
| } |
| |
| /** |
| * Returns a new LRUCache instance |
| */ |
| protected LRUCache<K, V> newInstance(int size) { |
| return new LRUCache<>(size); |
| } |
| |
| /** |
| * Answers the value in the cache at the given key. |
| * If the value is not in the cache, returns null |
| * |
| * This function does not modify timestamps. |
| */ |
| public V peek(K key) { |
| LRUCacheEntry<K, V> entry = this.entryTable.get(key); |
| if (entry == null) { |
| return null; |
| } |
| return entry.value; |
| } |
| |
| /** |
| * Adds an entry for the given key/value/space. |
| */ |
| protected void privateAdd (K key, V value, int space) { |
| LRUCacheEntry<K, V> entry; |
| entry = new LRUCacheEntry<>(key, value, space); |
| privateAddEntry (entry, false); |
| } |
| |
| /** |
| * Adds the given entry from the receiver. |
| * @param shuffle Indicates whether we are just shuffling the queue |
| * (in which case, the entry table is not modified). |
| */ |
| protected void privateAddEntry (LRUCacheEntry<K, V> entry, boolean shuffle) { |
| if (!shuffle) { |
| this.entryTable.put (entry.key, entry); |
| this.currentSpace += entry.space; |
| } |
| |
| entry.timestamp = this.timestampCounter++; |
| entry.next = this.entryQueue; |
| entry.previous = null; |
| |
| if (this.entryQueue == null) { |
| /* this is the first and last entry */ |
| this.entryQueueTail = entry; |
| } else { |
| this.entryQueue.previous = entry; |
| } |
| |
| this.entryQueue = entry; |
| } |
| |
| /** |
| * Removes the entry from the entry queue. |
| * @param shuffle indicates whether we are just shuffling the queue |
| * (in which case, the entry table is not modified). |
| */ |
| protected void privateRemoveEntry (LRUCacheEntry<K, V> entry, boolean shuffle) { |
| LRUCacheEntry<K, V> previous, next; |
| |
| previous = entry.previous; |
| next = entry.next; |
| |
| if (!shuffle) { |
| this.entryTable.remove(entry.key); |
| this.currentSpace -= entry.space; |
| } |
| |
| /* if this was the first entry */ |
| if (previous == null) { |
| this.entryQueue = next; |
| } else { |
| previous.next = next; |
| } |
| |
| /* if this was the last entry */ |
| if (next == null) { |
| this.entryQueueTail = previous; |
| } else { |
| next.previous = previous; |
| } |
| } |
| |
| /** |
| * Sets the value in the cache at the given key. Returns the value. |
| * |
| * @param key Key of object to add. |
| * @param value Value of object to add. |
| * @return added value. |
| */ |
| public V put(K key, V value) { |
| int newSpace, oldSpace, newTotal; |
| LRUCacheEntry<K, V> entry; |
| |
| /* Check whether there's an entry in the cache */ |
| newSpace = spaceFor(value); |
| entry = this.entryTable.get (key); |
| |
| if (entry != null) { |
| |
| /** |
| * Replace the entry in the cache if it would not overflow |
| * the cache. Otherwise flush the entry and re-add it so as |
| * to keep cache within budget |
| */ |
| oldSpace = entry.space; |
| newTotal = getCurrentSpace() - oldSpace + newSpace; |
| if (newTotal <= getSpaceLimit()) { |
| updateTimestamp (entry); |
| entry.value = value; |
| entry.space = newSpace; |
| this.currentSpace = newTotal; |
| return value; |
| } else { |
| privateRemoveEntry (entry, false); |
| } |
| } |
| if (makeSpace(newSpace)) { |
| privateAdd (key, value, newSpace); |
| } |
| return value; |
| } |
| |
| /** |
| * Removes and returns the value in the cache for the given key. |
| * If the key is not in the cache, returns null. |
| * |
| * @param key Key of object to remove from cache. |
| * @return Value removed from cache. |
| */ |
| public V removeKey (K key) { |
| LRUCacheEntry<K, V> entry = this.entryTable.get(key); |
| if (entry == null) { |
| return null; |
| } |
| V value = entry.value; |
| privateRemoveEntry (entry, false); |
| return value; |
| } |
| |
| /** |
| * Sets the maximum amount of space that the cache can store |
| * |
| * @param limit Number of units of cache space |
| */ |
| public void setSpaceLimit(int limit) { |
| if (limit < this.spaceLimit) { |
| makeSpace(this.spaceLimit - limit); |
| } |
| this.spaceLimit = limit; |
| } |
| |
| /** |
| * Returns the space taken by the given value. |
| */ |
| protected int spaceFor (V value) { |
| if (value instanceof ILRUCacheable) { |
| return ((ILRUCacheable) value).getCacheFootprint(); |
| } else { |
| return 1; |
| } |
| } |
| |
| /** |
| * Returns a String that represents the value of this object. This method |
| * is for debugging purposes only. |
| */ |
| @Override |
| public String toString() { |
| return |
| toStringFillingRation("LRUCache") + //$NON-NLS-1$ |
| toStringContents(); |
| } |
| |
| /** |
| * Returns a String that represents the contents of this object. This method |
| * is for debugging purposes only. |
| */ |
| protected String toStringContents() { |
| StringBuffer result = new StringBuffer(); |
| ToStringSorter<K> sorter = new ToStringSorter<>(o -> o instanceof JavaElement ? ((JavaElement) o).getElementName() : o.toString()); |
| List<Pair<K>> sortedObjects = sorter.sort(this.entryTable.keySet()); |
| for (Pair<K> pair : sortedObjects) { |
| String toString = pair.string; |
| V value = get(pair.object); |
| result.append(toString); |
| result.append(" -> "); //$NON-NLS-1$ |
| result.append(value); |
| result.append("\n"); //$NON-NLS-1$ |
| } |
| return result.toString(); |
| } |
| |
| public String toStringFillingRation(String cacheName) { |
| StringBuffer buffer = new StringBuffer(cacheName); |
| buffer.append('['); |
| buffer.append(getSpaceLimit()); |
| buffer.append("]: "); //$NON-NLS-1$ |
| buffer.append(NumberFormat.getInstance().format(fillingRatio())); |
| buffer.append("% full"); //$NON-NLS-1$ |
| return buffer.toString(); |
| } |
| |
| /** |
| * Updates the timestamp for the given entry, ensuring that the queue is |
| * kept in correct order. The entry must exist |
| */ |
| protected void updateTimestamp (LRUCacheEntry<K, V> entry) { |
| entry.timestamp = this.timestampCounter++; |
| if (this.entryQueue != entry) { |
| privateRemoveEntry (entry, true); |
| privateAddEntry (entry, true); |
| } |
| return; |
| } |
| } |