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