All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fiftyone.mobile.detection.cache.LruCache Maven / Gradle / Ivy

The newest version!
/* *********************************************************************
 * This Source Code Form is copyright of 51Degrees Mobile Experts Limited. 
 * Copyright © 2017 51Degrees Mobile Experts Limited, 5 Charlotte Close,
 * Caversham, Reading, Berkshire, United Kingdom RG4 7BY
 * 
 * This Source Code Form is the subject of the following patents and patent
 * applications, owned by 51Degrees Mobile Experts Limited of 5 Charlotte
 * Close, Caversham, Reading, Berkshire, United Kingdom RG4 7BY: 
 * European Patent No. 2871816;
 * European Patent Application No. 17184134.9;
 * United States Patent Nos. 9,332,086 and 9,350,823; and
 * United States Patent Application No. 15/686,066.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.
 * 
 * If a copy of the MPL was not distributed with this file, You can obtain
 * one at http://mozilla.org/MPL/2.0/.
 * 
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
 * ********************************************************************* */
package fiftyone.mobile.detection.cache;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Many of the entities used by the detector data set are requested repeatedly. 
 * The cache improves memory usage and reduces strain on the garbage collector
 * by storing previously requested entities for a period of time to avoid the 
 * need to re-fetch them from the underlying storage mechanism.
 * 

* A variation of a Least Recently Used (LRU) cache is used. LRU cache keeps * track of what was used when in order to discard the least recently used * items first. Every time a cache item is used the "age" of the item used * is updated. *

* This implementation supports concurrency by using multiple linked lists * in place of a single linked list in the original implementation. * The linked list to use is assigned at random and stored in the cached * item. This will generate an even set of results across the different * linked lists. The approach reduces the probability of the same linked * list being locked when used in a environments with a high degree of * concurrency. If the feature is not required then the constructor should be * provided with a concurrency value of 1. *

* For a vast majority of the real life environments a constant stream of unique * User-Agents is a fairly rare event. Usually the same User-Agent can be * encountered multiple times within a fairly short period of time as the user * is making a subsequent request. Caching frequently occurring User-Agents * improved detection speed considerably. *

* Some devices are also more popular than others and while the User-Agents for * such devices may differ, the combination of components used would be very * similar. Therefore internal caching is also used to take advantage of the * more frequently occurring entities. *

* This class should not be called as it is part of the internal logic. * * @param Key for the cache items. * @param Value for the cache items. */ public class LruCache implements ILoadingCache { /** * An item stored in the cache along with references to the next and * previous items. */ class CachedItem { /** * Key associated with the cached item. */ final K key; /** * Value of the cached item. */ final V value; /** * The next item in the linked list. */ CachedItem next; /** * The previous item in the linked list. */ CachedItem previous; /** * The linked list the item is part of. */ final CacheLinkedList list; /** * Indicates that the item is valid and added to the linked list. * It is not in the process of being manipulated by another thread * either being added to the list or being removed. */ boolean isValid; public CachedItem(CacheLinkedList list, K key, V value) { this.list = list; this.key = key; this.value = value; } } /** * A linked list used in the LruCache implementation. * This linked list implementation enables items to be moved * within the linked list. */ class CacheLinkedList { /** * The cache that the list is part of. */ LruCache cache = null; /** * The first item in the list. */ CachedItem first = null; /** * The last item in the list. */ CachedItem last = null; /** * Constructs a new instance of the CacheLinkedList. */ public CacheLinkedList(LruCache cache) { this.cache = cache; } /** * Adds a new cache item to the linked list. */ void addNew(CachedItem item) { boolean added = false; if (item != first) { synchronized(this) { if (item != first) { if (first == null) { // First item to be added to the queue. first = item; last = item; } else { // Add this item to the head of the linked list. item.next = first; first.previous = item; first = item; // Set flag to indicate an item was added and if // the cache is full an item should be removed. added = true; } // Indicate the item is now ready for another thread // to manipulate and is fully added to the linked list. item.isValid = true; } } } // Check if the linked list needs to be trimmed as the cache // size has been exceeded. if (added && cache.hashMap.size() > cache.cacheSize) { synchronized (this) { if (cache.hashMap.size() > cache.cacheSize) { // Indicate that the last item is being removed from // the linked list. last.isValid = false; // Remove the item from the dictionary before // removing from the linked list. cache.hashMap.remove(last.key); last = last.previous; last.next = null; } } } } /** * Set the first item in the linked list to the item provided. */ void moveFirst(CachedItem item) { if (item != first && item.isValid == true) { synchronized (this) { if (item != first && item.isValid == true) { if (item == last) { // The item is the last one in the list so is // easy to remove. A new last will need to be // set. last = item.previous; last.next = null; } else { // The item was not at the end of the list. // Remove it from it's current position ready // to be added to the top of the list. item.previous.next = item.next; item.next.previous = item.previous; } // Add this item to the head of the linked list. item.next = first; item.previous = null; first.previous = item; first = item; } } } } /** * Clears all items from the linked list. */ void clear() { first = null; last = null; } } /** * Loader used to fetch items not in the cache. */ public void setCacheLoader(IValueLoader loader) { this.loader = loader; } private IValueLoader loader; /** * Hash map of keys to item values. */ private final ConcurrentHashMap hashMap; /** * A array of doubly linked lists. Not marked private so that the unit * test can check the elements. */ final CacheLinkedList[] linkedLists; /** * Random number generator used to select the linked list to use with * the new item being added to the cache. */ final Random random = new Random(); /** * Constructs a new instance of the cache. * @param cacheSize The number of items to store in the cache. */ public LruCache(int cacheSize) { this(cacheSize, null); } /** * Constructs a new instance of the cache. * * @param cacheSize The number of items to store in the cache. * @param loader used to fetch items not in the cache. */ public LruCache(int cacheSize, IValueLoader loader) { this(cacheSize, Runtime.getRuntime().availableProcessors(), loader); } /** * Constructs a new instance of the cache. * * @param cacheSize The number of items to store in the cache. * @param loader used to fetch items not in the cache. */ public LruCache(int cacheSize, int concurrency, IValueLoader loader) { if (concurrency <= 0) { throw new IllegalArgumentException( "Concurrency must be a positive integer greater than 0."); } this.cacheSize = cacheSize; this.loader = loader; this.hashMap = new ConcurrentHashMap(cacheSize); linkedLists = (CacheLinkedList[]) Array.newInstance( CacheLinkedList.class, concurrency); for(int i = 0; i < linkedLists.length; i++){ linkedLists[i] = new CacheLinkedList(this); } } /** * The number of items the cache lists should have capacity for. * * @return capacity of the cache. */ @Override public long getCacheSize() { return cacheSize; } private int cacheSize; /** * @return number of cache misses. */ @Override public long getCacheMisses() { return misses.get(); } private final AtomicLong misses = new AtomicLong(0); /** * @return number of requests received by the cache. */ @Override public long getCacheRequests() { return requests.get(); } private final AtomicLong requests = new AtomicLong(0); /** * @return the percentage of times cache request did not return a result. */ @Override public double getPercentageMisses() { return misses.doubleValue()/ requests.doubleValue(); } /** * Retrieves the value for key requested. If the key does not exist * in the cache then the Fetch method of the cache's loader is used to * retrieve the value. * * @param key or the item required. * @return An instance of the value associated with the key. * @throws java.lang.IllegalStateException if there was a problem accessing data file. */ @Override public V get(K key) { try { return get(key, loader); } catch (IOException e) { throw new IllegalStateException(e); } } /** * Retrieves the value for key requested. If the key does not exist * in the cache then the Fetch method is used to retrieve the value * from another loader. * * @param key or the item required * @param loader to fetch the items from * @return An instance of the value associated with the key * @throws java.io.IOException if there was a problem accessing data file. */ public V get(K key, IValueLoader loader) throws IOException { boolean added = false; requests.incrementAndGet(); // First, try to get the item from the hashMap CachedItem node = hashMap.get(key); if (node == null) { misses.incrementAndGet(); // The item was not in the cache so need to load it // before trying to add it in. // Also get a randomly selected linked list to add // the item to. CachedItem newNode = new CachedItem( GetRandomLinkedList(), key, loader.load(key)); // If the node has already been added to the dictionary // then get it, otherwise add the one just fetched. node = hashMap.putIfAbsent(key, newNode); // If the node was absent and was added to the dictionary (node == null) // then it needs to be added to the linked list. if (node == null) { added = true; newNode.list.addNew(newNode); node = newNode; } } if (added == false) { // The item is in the dictionary. // Move the item to the head of it's LRU list. node.list.moveFirst(node); } return node.value; } /** * Resets the 'stats' for the cache. */ @Override public void resetCache() { this.hashMap.clear(); misses.set(0); requests.set(0); for(int i = 0; i < linkedLists.length; i++){ linkedLists[i].clear(); } } /** * Returns a random linked list. */ private CacheLinkedList GetRandomLinkedList() { return linkedLists[random.nextInt(linkedLists.length)]; } /** * Return builder for an LRU cache */ public static ICacheBuilder builder() { return new LruBuilder(); } public static class LruBuilder implements ICacheBuilder { public ICache build(int size) { return new LruCache(size); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy