1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #pragma once 18 19 #include <functional> 20 #include <iterator> 21 #include <list> 22 #include <mutex> 23 #include <optional> 24 #include <thread> 25 #include <unordered_map> 26 27 #include "common/list_map.h" 28 #include "os/log.h" 29 30 namespace bluetooth { 31 namespace common { 32 33 // An LRU map-cache the evict the oldest item when reaching capacity 34 // 35 // Usage: 36 // - keys are sorted from warmest to coldest 37 // - iterating through the cache won't warm up keys 38 // - operations on iterators won't warm up keys 39 // - find(), contains(), insert_or_assign() will warm up the key 40 // - insert_or_assign() will evict coldest key when cache reaches capacity 41 // - NOT THREAD SAFE 42 // 43 // Performance: 44 // - Key look-up and modification is O(1) 45 // - Memory consumption is: 46 // O(2*capacity*sizeof(K) + capacity*(sizeof(nullptr)+sizeof(V))) 47 // 48 // Template: 49 // - Key key type 50 // - T value type 51 // */ 52 template <typename Key, typename T> 53 class LruCache { 54 public: 55 using value_type = typename ListMap<Key, T>::value_type; 56 // different from c++17 node_type on purpose as we want node to be copyable 57 using node_type = typename ListMap<Key, T>::node_type; 58 using iterator = typename ListMap<Key, T>::iterator; 59 using const_iterator = typename ListMap<Key, T>::const_iterator; 60 61 // Constructor a LRU cache with |capacity| LruCache(size_t capacity)62 explicit LruCache(size_t capacity) : capacity_(capacity) { 63 ASSERT_LOG(capacity_ != 0, "Unable to have 0 LRU Cache capacity"); 64 } 65 66 // for move 67 LruCache(LruCache&& other) noexcept = default; 68 LruCache& operator=(LruCache&& other) noexcept = default; 69 70 // copy-constructor 71 // iterators in key_map_ cannot be copied directly LruCache(const LruCache & other)72 LruCache(const LruCache& other) : capacity_(other.capacity_), list_map_(other.list_map_) {} 73 74 // copy-assignment 75 // iterators in key_map_ cannot be copied directly 76 LruCache& operator=(const LruCache& other) { 77 if (&other == this) { 78 return *this; 79 } 80 capacity_ = other.capacity_; 81 list_map_ = other.list_map_; 82 return *this; 83 } 84 85 // comparison operators 86 bool operator==(const LruCache& rhs) const { 87 return capacity_ == rhs.capacity_ && list_map_ == rhs.list_map_; 88 } 89 bool operator!=(const LruCache& rhs) const { 90 return !(*this == rhs); 91 } 92 ~LruCache()93 ~LruCache() { 94 clear(); 95 } 96 97 // Clear the cache clear()98 void clear() { 99 list_map_.clear(); 100 } 101 102 // Find the value of a key, and move the key to the head of cache, if there is one. Return iterator to value if key 103 // exists, end() if not. Iterator might be invalidated when removed or evicted. Const version. 104 // 105 // LRU: Will warm up key 106 // LRU: Access to returned iterator won't move key in LRU find(const Key & key)107 const_iterator find(const Key& key) const { 108 return const_cast<LruCache*>(this)->find(key); 109 } 110 111 // Find the value of a key, and move the key to the head of cache, if there is one. Return iterator to value if key 112 // exists, end() if not. Iterator might be invalidated when removed or evicted 113 // 114 // LRU: Will warm up key 115 // LRU: Access to returned iterator won't move key in LRU find(const Key & key)116 iterator find(const Key& key) { 117 auto iter = list_map_.find(key); 118 if (iter == list_map_.end()) { 119 return end(); 120 } 121 // move to front 122 list_map_.splice(list_map_.begin(), list_map_, iter); 123 return iter; 124 } 125 126 // Check if key exist in the cache. Return true if key exist in cache, false, if not 127 // 128 // LRU: Will warm up key contains(const Key & key)129 bool contains(const Key& key) const { 130 return find(key) != list_map_.end(); 131 } 132 133 // Put a key-value pair to the head of cache, evict the oldest key if cache is at capacity. Eviction is based on key 134 // ONLY. Hence, updating a key will not evict the oldest key. Return evicted value if old value was evicted, 135 // std::nullopt if not. The return value will be evaluated to true in a boolean context if a value is contained by 136 // std::optional, false otherwise. 137 // 138 // LRU: Will warm up key insert_or_assign(const Key & key,T value)139 std::optional<node_type> insert_or_assign(const Key& key, T value) { 140 if (contains(key)) { 141 // contains() calls find() that moved the node to the head 142 list_map_.begin()->second = std::move(value); 143 return std::nullopt; 144 } 145 // remove tail if at capacity 146 std::optional<node_type> evicted_node = std::nullopt; 147 if (list_map_.size() == capacity_) { 148 evicted_node = list_map_.extract(std::prev(list_map_.end())->first); 149 } 150 // insert new one to front of list 151 list_map_.insert_or_assign(list_map_.begin(), key, std::move(value)); 152 return evicted_node; 153 } 154 155 // Put a key-value pair to the head of cache, evict the oldest key if cache is at capacity. Eviction is based on key 156 // ONLY. Hence, updating a key will not evict the oldest key. This method tries to construct the value in-place. If 157 // the key already exist, this method only update the value. Return inserted iterator, whether insertion happens, and 158 // evicted value if old value was evicted or std::nullopt 159 // 160 // LRU: Will warm up key 161 template <class... Args> try_emplace(const Key & key,Args &&...args)162 std::tuple<iterator, bool, std::optional<node_type>> try_emplace(const Key& key, Args&&... args) { 163 if (contains(key)) { 164 // contains() calls find() that moved the node to the head 165 return std::make_tuple(end(), false, std::nullopt); 166 } 167 // remove tail if at capacity 168 std::optional<node_type> evicted_node = std::nullopt; 169 if (list_map_.size() == capacity_) { 170 evicted_node = list_map_.extract(std::prev(list_map_.end())->first); 171 } 172 // insert new one to front of list 173 auto pair = list_map_.try_emplace(list_map_.begin(), key, std::forward<Args>(args)...); 174 return std::make_tuple(pair.first, pair.second, std::move(evicted_node)); 175 } 176 177 // Delete a key from cache, return removed value if old value was evicted, std::nullopt if not. The return value will 178 // be evaluated to true in a boolean context if a value is contained by std::optional, false otherwise. extract(const Key & key)179 inline std::optional<node_type> extract(const Key& key) { 180 return list_map_.extract(key); 181 } 182 183 /// Remove an iterator pointed item from the lru cache and return the iterator immediately after the erased item erase(const_iterator iter)184 iterator erase(const_iterator iter) { 185 return list_map_.erase(iter); 186 } 187 188 // Return size of the cache size()189 inline size_t size() const { 190 return list_map_.size(); 191 } 192 193 // Iterator interface for begin begin()194 inline iterator begin() { 195 return list_map_.begin(); 196 } 197 198 // Return iterator interface for begin, const begin()199 inline const_iterator begin() const { 200 return list_map_.begin(); 201 } 202 203 // Return iterator interface for end end()204 inline iterator end() { 205 return list_map_.end(); 206 } 207 208 // Iterator interface for end, const end()209 inline const_iterator end() const { 210 return list_map_.end(); 211 } 212 213 private: 214 size_t capacity_; 215 ListMap<Key, T> list_map_; 216 }; 217 218 } // namespace common 219 } // namespace bluetooth 220