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