# Nimbus # Copyright (c) 2018 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or # http://www.apache.org/licenses/LICENSE-2.0) # * MIT license ([LICENSE-MIT](LICENSE-MIT) or # http://opensource.org/licenses/MIT) # at your option. This file may not be copied, modified, or distributed except # according to those terms. ## Hash as hash can: LRU cache ## =========================== ## ## This module provides a generic last-recently-used cache data structure. ## ## The implementation works with the same complexity as the worst case of a ## nim hash tables operation. This is is assumed to be O(1) in most cases ## (so long as the table does not degrade into one-bucket linear mode, or ## some bucket-adjustment algorithm takes over.) ## ## For consistency with every other data type in Nim these have value ## semantics, this means that `=` performs a deep copy of the LRU cache. ## import math, eth/rlp, stew/results, tables export results type LruKey*[T,K] = ## User provided handler function, derives an ## LRU `key` from function argument `arg`. The ## `key` is used to index the cache data. proc(arg: T): K {.gcsafe, raises: [Defect,CatchableError].} LruValue*[T,V,E] = ## User provided handler function, derives an ## LRU `value` from function argument `arg`. proc(arg: T): Result[V,E] {.gcsafe, raises: [Defect,CatchableError].} LruItem*[K,V] = ## Doubly linked hash-tab item encapsulating ## the `value` (which is the result from ## `LruValue` handler function. tuple[prv, nxt: K, value: V] # There could be {.rlpCustomSerialization.} annotation for the tab field. # As there was a problem with the automatic Rlp serialisation for generic # type, the easier solution was an all manual read()/append() for the whole # generic LruCacheData[K,V] type. LruData[K,V] = object maxItems: int ## Max number of entries first, last: K ## Doubly linked item list queue tab: Table[K,LruItem[K,V]] ## (`key`,encapsulated(`value`)) data table LruCache*[T,K,V,E] = object data*: LruData[K,V] ## Cache data, can be serialised toKey: LruKey[T,K] ## Handler function, derives `key` toValue: LruValue[T,V,E] ## Handler function, derives `value` {.push raises: [Defect].} # ------------------------------------------------------------------------------ # Private functions # ------------------------------------------------------------------------------ proc `==`[K,V](a, b: var LruData[K,V]): bool = a.maxItems == b.maxItems and a.first == b.first and a.last == b.last and a.tab == b.tab # ------------------------------------------------------------------------------ # Public constructor and reset # ------------------------------------------------------------------------------ proc clearCache*[T,K,V,E](cache: var LruCache[T,K,V,E]; cacheInitSize = 0) {.gcsafe, raises: [Defect].} = ## Reset/clear an initialised LRU cache. The cache will be re-allocated ## with `cacheInitSize` initial spaces if this is positive, or `cacheMaxItems` ## spaces (see `initLruCache()`) as a default. var initSize = cacheInitSize if initSize <= 0: initSize = cache.data.maxItems cache.data.first.reset cache.data.last.reset cache.data.tab = initTable[K,LruItem[K,V]](initSize.nextPowerOfTwo) proc initCache*[T,K,V,E](cache: var LruCache[T,K,V,E]; toKey: LruKey[T,K], toValue: LruValue[T,V,E]; cacheMaxItems = 10; cacheInitSize = 0) {.gcsafe, raises: [Defect].} = ## Initialise LRU cache. The handlers `toKey()` and `toValue()` are explained ## at the data type definition. The cache will be allocated with ## `cacheInitSize` initial spaces if this is positive, or `cacheMaxItems` ## spaces (see `initLruCache()`) as a default. cache.data.maxItems = cacheMaxItems cache.toKey = toKey cache.toValue = toValue cache.clearCache # ------------------------------------------------------------------------------ # Public functions, basic mechanism # ------------------------------------------------------------------------------ proc getItem*[T,K,V,E](lru: var LruCache[T,K,V,E]; arg: T; peekOk = false): Result[V,E] {.gcsafe, raises: [Defect,CatchableError].} = ## If the key `lru.toKey(arg)` is a cached key, the associated value will ## be returnd. If the `peekOK` argument equals `false`, the associated ## key-value pair will have been moved to the end of the LRU queue. ## ## If the key `lru.toKey(arg)` is not a cached key and the LRU queue has at ## least `cacheMaxItems` entries (see `initLruCache()`, the first key-value ## pair will be removed from the LRU queue. Then the value the pair ## (`lru.toKey(arg)`,`lru.toValue(arg)`) will be appended to the LRU queue ## and the value part returned. ## let key = lru.toKey(arg) # Relink item if already in the cache => move to last position if lru.data.tab.hasKey(key): let lruItem = lru.data.tab[key] if peekOk or key == lru.data.last: # Nothing to do return ok(lruItem.value) # Unlink key Item if key == lru.data.first: lru.data.first = lruItem.nxt else: lru.data.tab[lruItem.prv].nxt = lruItem.nxt lru.data.tab[lruItem.nxt].prv = lruItem.prv # Append key item lru.data.tab[lru.data.last].nxt = key lru.data.tab[key].prv = lru.data.last lru.data.last = key return ok(lruItem.value) # Calculate value, pass through error unless OK let rcValue = ? lru.toValue(arg) # Limit number of cached items if lru.data.maxItems <= lru.data.tab.len: # Delete oldest/first entry var nextKey = lru.data.tab[lru.data.first].nxt lru.data.tab.del(lru.data.first) lru.data.first = nextKey # Add cache entry var tabItem: LruItem[K,V] # Initialise empty queue if lru.data.tab.len == 0: lru.data.first = key lru.data.last = key else: # Append queue item lru.data.tab[lru.data.last].nxt = key tabItem.prv = lru.data.last lru.data.last = key tabItem.value = rcValue lru.data.tab[key] = tabItem result = ok(rcValue) # ------------------------------------------------------------------------------ # Public functions, cache info # ------------------------------------------------------------------------------ proc hasKey*[T,K,V,E](lru: var LruCache[T,K,V,E]; arg: T): bool {.gcsafe.} = ## Check whether the `arg` argument is cached let key = lru.toKey(arg) lru.data.tab.hasKey(key) proc firstKey*[T,K,V,E](lru: var LruCache[T,K,V,E]): K {.gcsafe.} = ## Returns the key of the first item in the LRU queue, or the reset ## value it the cache is empty. if 0 < lru.data.tab.len: result = lru.data.first proc lastKey*[T,K,V,E](lru: var LruCache[T,K,V,E]): K {.gcsafe.} = ## Returns the key of the last item in the LRU queue, or the reset ## value it the cache is empty. if 0 < lru.data.tab.len: result = lru.data.last proc maxLen*[T,K,V,E](lru: var LruCache[T,K,V,E]): int {.gcsafe.} = ## Maximal number of cache entries. lru.data.maxItems proc len*[T,K,V,E](lru: var LruCache[T,K,V,E]): int {.gcsafe.} = ## Return the number of elements in the cache. lru.data.tab.len # ------------------------------------------------------------------------------ # Public functions, advanced features # ------------------------------------------------------------------------------ proc setItem*[T,K,V,E](lru: var LruCache[T,K,V,E]; arg: T; value: V): bool {.gcsafe, raises: [Defect,CatchableError].} = ## Update entry with key `lru.toKey(arg)` by `value`. Reurns `true` if the ## key exists in the database, and false otherwise. ## ## This function allows for simlifying the `toValue()` function (see ## `initLruCache()`) to provide a placeholder only and later fill this ## slot with this `setLruItem()` function. let key = lru.toKey(arg) if lru.data.tab.hasKey(key): lru.data.tab[key].value = value return true proc delItem*[T,K,V,E](lru: var LruCache[T,K,V,E]; arg: T): bool {.gcsafe, discardable, raises: [Defect,KeyError].} = ## Delete the `arg` argument from cached. That way, the LRU cache can ## be re-purposed as a sequence with efficient random delete facility. let key = lru.toKey(arg) # Relink item if already in the cache => move to last position if lru.data.tab.hasKey(key): let lruItem = lru.data.tab[key] # Unlink key Item if lru.data.tab.len == 1: lru.data.first.reset lru.data.last.reset elif key == lru.data.last: lru.data.last = lruItem.prv elif key == lru.data.first: lru.data.first = lruItem.nxt else: lru.data.tab[lruItem.prv].nxt = lruItem.nxt lru.data.tab[lruItem.nxt].prv = lruItem.prv lru.data.tab.del(key) return true iterator keyItemPairs*[T,K,V,E](lru: var LruCache[T,K,V,E]): (K,LruItem[K,V]) {.gcsafe, raises: [Defect,CatchableError].} = ## Cycle through all (key,lruItem) pairs in chronological order. if 0 < lru.data.tab.len: var key = lru.data.first for _ in 0 ..< lru.data.tab.len - 1: var item = lru.data.tab[key] yield (key, item) key = item.nxt yield (key, lru.data.tab[key]) if key != lru.data.last: raiseAssert "Garbled LRU cache next/prv references" # ------------------------------------------------------------------------------ # Public functions, RLP support # ------------------------------------------------------------------------------ proc `==`*[T,K,V,E](a, b: var LruCache[T,K,V,E]): bool = ## Returns `true` if both argument LRU caches contain the same data ## regardless of `toKey()`/`toValue()` handler functions. a.data == b.data proc append*[K,V](rw: var RlpWriter; data: LruData[K,V]) {. inline, raises: [Defect,KeyError].} = ## Generic support for `rlp.encode(lru.data)` for serialising the data ## part of an LRU cache. rw.append(data.maxItems) rw.append(data.first) rw.append(data.last) rw.startList(data.tab.len) # store keys in LRU order if 0 < data.tab.len: var key = data.first for _ in 0 ..< data.tab.len - 1: var value = data.tab[key] rw.append((key, value)) key = value.nxt rw.append((key, data.tab[key])) if key != data.last: raiseAssert "Garbled LRU cache next/prv references" proc read*[K,V](rlp: var Rlp; Q: type LruData[K,V]): Q {. inline, raises: [Defect,RlpError].} = ## Generic support for `rlp.decode(bytes)` for loading the data part ## of an LRU cache from a serialised data stream. result.maxItems = rlp.read(int) result.first = rlp.read(K) result.last = rlp.read(K) for w in rlp.items: let (key,value) = w.read((K,LruItem[K,V])) result.tab[key] = value # ------------------------------------------------------------------------------ # End # ------------------------------------------------------------------------------