utils: add LRU cache to RefCountedMap (#9730)
* utils: add LRU cache to RefCountedMap This change introduces a new data structure LruCache and uses it in RefCountedMap to keep a fixed number of cache entries alive after their reference count has dropped to zero in the main map. * utils: address LRU cache comments
This commit is contained in:
@@ -39,12 +39,25 @@ bool MaterialCache::MaterialKey::operator==(MaterialKey const& rhs) const noexce
|
||||
return parser == rhs.parser;
|
||||
}
|
||||
|
||||
MaterialCache::MaterialCache()
|
||||
: mDefinitions("MaterialCache::mDefinitions", 0),
|
||||
mPrograms("MaterialCache::mPrograms", 0) {}
|
||||
|
||||
MaterialCache::~MaterialCache() {
|
||||
assert_invariant(mDefinitions.empty());
|
||||
assert_invariant(mPrograms.empty());
|
||||
assert_invariant(mSpecializationConstantsInternPool.empty());
|
||||
}
|
||||
|
||||
void MaterialCache::terminate(FEngine& engine) {
|
||||
mPrograms.clearLruCache([&engine](backend::Handle<backend::HwProgram>& program) {
|
||||
engine.getDriverApi().destroyProgram(program);
|
||||
});
|
||||
mDefinitions.clearLruCache([&engine](MaterialDefinition& definition) {
|
||||
definition.terminate(engine);
|
||||
});
|
||||
}
|
||||
|
||||
MaterialDefinition* UTILS_NULLABLE MaterialCache::acquireMaterial(FEngine& engine,
|
||||
const void* UTILS_NONNULL data, size_t size) noexcept {
|
||||
std::unique_ptr<MaterialParser> parser = MaterialDefinition::createParser(engine.getBackend(),
|
||||
|
||||
@@ -56,8 +56,13 @@ public:
|
||||
using ProgramCache =
|
||||
utils::RefCountedMap<ProgramSpecialization, backend::Handle<backend::HwProgram>>;
|
||||
|
||||
MaterialCache();
|
||||
~MaterialCache();
|
||||
|
||||
// All reference-counted resources should be freed by the time MaterialCache is destructed, but
|
||||
// the LRU cache needs to be explicitly freed in addition.
|
||||
void terminate(FEngine& engine);
|
||||
|
||||
SpecializationConstantInternPool& getSpecializationConstantsInternPool() {
|
||||
return mSpecializationConstantsInternPool;
|
||||
}
|
||||
|
||||
@@ -596,6 +596,9 @@ void FEngine::shutdown() {
|
||||
DLOG(INFO) << "CircularBuffer: High watermark " << wm / 1024 << " KiB (" << wmpct << "%)";
|
||||
#endif
|
||||
|
||||
/* Destroy any leftover items in the cache. */
|
||||
mMaterialCache.terminate(*this);
|
||||
|
||||
DriverApi& driver = getDriverApi();
|
||||
|
||||
/*
|
||||
|
||||
@@ -31,6 +31,7 @@ set(DIST_HDRS
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/Invocable.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/Log.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/Logger.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/LruCache.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/memalign.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/MonotonicRingMap.h
|
||||
${PUBLIC_HDR_DIR}/${TARGET}/Mutex.h
|
||||
@@ -181,6 +182,7 @@ if (FILAMENT_BUILD_TESTING)
|
||||
test/test_FixedCircularBuffer.cpp
|
||||
test/test_Hash.cpp
|
||||
test/test_InternPool.cpp
|
||||
test/test_LruCache.cpp
|
||||
test/test_JobSystem.cpp
|
||||
test/test_MonotonicRingMap.cpp
|
||||
test/test_QuadTreeArray.cpp
|
||||
|
||||
322
libs/utils/include/utils/LruCache.h
Normal file
322
libs/utils/include/utils/LruCache.h
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef TNT_UTILS_LRUCACHE_H
|
||||
#define TNT_UTILS_LRUCACHE_H
|
||||
|
||||
#include <utils/Allocator.h>
|
||||
#include <utils/compiler.h>
|
||||
|
||||
#include <tsl/robin_map.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace utils {
|
||||
|
||||
/**
|
||||
* A fixed-capacity Least Recently Used (LRU) cache.
|
||||
*
|
||||
* This container allows O(1) access, insertion, and eviction. It uses an Arena with an
|
||||
* ObjectPoolAllocator to store the nodes of a doubly-linked list representing the LRU order,
|
||||
* and a robin_map for fast key lookups.
|
||||
*
|
||||
* This class is not thread-safe. Additionally, the pop() and put() methods have special concerns
|
||||
* with regards to object lifetimes.
|
||||
*
|
||||
* @tparam Key The type of the keys.
|
||||
* @tparam T The type of the values.
|
||||
* @tparam Hash The hasher for the keys.
|
||||
*/
|
||||
template<typename Key,
|
||||
typename T,
|
||||
typename Hash = std::hash<Key>,
|
||||
typename KeyEqual = std::equal_to<Key>>
|
||||
class LruCache {
|
||||
// If Key is bigger than a pointer, robin map keys are references to Node::key. Otherwise,
|
||||
// they're full copies.
|
||||
using MapKey = std::conditional_t<(sizeof(Key) > sizeof(void*)),
|
||||
std::reference_wrapper<const Key>, Key>;
|
||||
|
||||
public:
|
||||
using size_type = uint32_t;
|
||||
|
||||
/**
|
||||
* Creates an LRU cache with the specified capacity.
|
||||
*
|
||||
* @param name Name to identify the backing Arena allocator.
|
||||
* @param capacity The maximum number of elements the cache can hold.
|
||||
*/
|
||||
LruCache(const char* UTILS_NONNULL name, size_type capacity)
|
||||
: mCapacity(capacity),
|
||||
// HACK: FreeList cannot handle a capacity of 0.
|
||||
mArena(name, (capacity ? capacity : 1) * sizeof(Node)),
|
||||
mHead(nullptr),
|
||||
mTail(nullptr) {
|
||||
mMap.reserve(capacity);
|
||||
}
|
||||
|
||||
~LruCache() {
|
||||
clear();
|
||||
}
|
||||
|
||||
LruCache(const LruCache&) = delete;
|
||||
LruCache& operator=(const LruCache&) = delete;
|
||||
LruCache(LruCache&&) = default;
|
||||
LruCache& operator=(LruCache&&) = default;
|
||||
|
||||
/**
|
||||
* Returns a reference to the value associated with the key.
|
||||
*
|
||||
* Additionally moves the key/value pair to the front of the most recently used (MRU) list.
|
||||
*
|
||||
* @param key The key to look up.
|
||||
* @param hash The precomputed hash of the key.
|
||||
* @return Pointer to the value, if exists; otherwise, nullptr.
|
||||
*/
|
||||
T* UTILS_NULLABLE get(Key const& key, size_t hash) {
|
||||
auto it = mMap.find(key, hash);
|
||||
if (UTILS_UNLIKELY(it == mMap.end())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Node* node = it->second;
|
||||
assert(node != nullptr);
|
||||
|
||||
moveToFront(node);
|
||||
|
||||
return &node->value;
|
||||
}
|
||||
|
||||
inline T* UTILS_NULLABLE get(Key const& key) {
|
||||
return get(key, Hash{}(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the value out of the cache, if it exists.
|
||||
*
|
||||
* Because this moves the actual value, any previous references to this object returned by a
|
||||
* call to get() or put() is invalidated by this call.
|
||||
*
|
||||
* @param key The key to look up.
|
||||
* @param hash The precomputed hash of the key.
|
||||
*/
|
||||
std::optional<T> pop(Key const& key, size_t hash) {
|
||||
auto it = mMap.find(key, hash);
|
||||
if (UTILS_UNLIKELY(it == mMap.end())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Node* node = it->second;
|
||||
assert(node != nullptr);
|
||||
|
||||
if (node == mHead && node == mTail) {
|
||||
mHead = nullptr;
|
||||
mTail = nullptr;
|
||||
} else if (node == mHead) {
|
||||
assert(node->next != nullptr);
|
||||
mHead = node->next;
|
||||
mHead->prev = nullptr;
|
||||
} else if (node == mTail) {
|
||||
assert(node->prev != nullptr);
|
||||
mTail = node->prev;
|
||||
mTail->next = nullptr;
|
||||
} else {
|
||||
assert(node->prev != nullptr);
|
||||
assert(node->next != nullptr);
|
||||
node->prev->next = node->next;
|
||||
node->next->prev = node->prev;
|
||||
}
|
||||
|
||||
T r = std::move(node->value);
|
||||
mMap.erase(it);
|
||||
mArena.destroy(node);
|
||||
return r;
|
||||
}
|
||||
|
||||
inline std::optional<T> pop(Key const& key) {
|
||||
return pop(key, Hash{}(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new entry or updates an existing one.
|
||||
*
|
||||
* Prepends it to the front of the most recently used (MRU) list. Potentially evicts the
|
||||
* least-recently used (LRU) key/value pair by calling the releaser function.
|
||||
*
|
||||
* Due to the evicting nature of this function, any pointers to any objects within the LRU cache
|
||||
* that had been previously returned by a call to get() or another call to put() can be
|
||||
* considered INVALID and any access to them will result in undefined behavior.
|
||||
*
|
||||
* An evicted node is removed from the map before the releaser callback is called. Calls to
|
||||
* get() or pop() in the body of the releaser for the item in question will fail.
|
||||
*
|
||||
* @tparam F Callable type accepting T&&.
|
||||
* @param key The key to insert or update.
|
||||
* @param t The value to insert or update.
|
||||
* @param hash The precomputed hash of the key.
|
||||
* @param releaser Function called with the evicted value (T&&) if eviction occurs.
|
||||
* @return Reference to the inserted or updated value.
|
||||
*/
|
||||
template<typename F>
|
||||
T& put(Key key, T value, size_t hash, F releaser) {
|
||||
// Assert that we have capacity to store at least one item
|
||||
assert(mCapacity > 0);
|
||||
|
||||
auto it = mMap.find(key, hash);
|
||||
if (it != mMap.end()) {
|
||||
Node* node = it->second;
|
||||
assert(node != nullptr);
|
||||
|
||||
moveToFront(node);
|
||||
|
||||
node->value = std::move(value);
|
||||
return node->value;
|
||||
}
|
||||
|
||||
if (UTILS_LIKELY(mMap.size() >= mCapacity)) {
|
||||
evict(releaser);
|
||||
}
|
||||
|
||||
// Create new node at front of list.
|
||||
Node* node = mArena.template make<Node>(std::move(key), std::move(value), mHead);
|
||||
assert(node != nullptr);
|
||||
attach(node);
|
||||
|
||||
mMap.emplace(node->key, node);
|
||||
|
||||
return node->value;
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
inline T& put(Key key, T value, F releaser) {
|
||||
return put(std::move(key), std::move(value), Hash{}(key), std::move(releaser));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache, calling releaser on each item evicted.
|
||||
*
|
||||
* @param releaser Function called with the evicted value (T&&)
|
||||
*/
|
||||
template<typename F>
|
||||
void clear(F releaser) {
|
||||
// Clear map first, just in case it tries to dereference any keys.
|
||||
mMap.clear();
|
||||
// Destroy everything in the arena.
|
||||
Node* node = mHead;
|
||||
while (UTILS_LIKELY(node)) {
|
||||
Node* next = node->next;
|
||||
releaser(std::move(node->value));
|
||||
mArena.destroy(node);
|
||||
node = next;
|
||||
}
|
||||
mHead = nullptr;
|
||||
mTail = nullptr;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
clear([](T&&){});
|
||||
}
|
||||
|
||||
/** Returns the number of elements in the cache. */
|
||||
size_type size() const noexcept { return mMap.size(); }
|
||||
|
||||
/** Returns the capacity of the cache. */
|
||||
size_type capacity() const noexcept { return mCapacity; }
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
Key key;
|
||||
T value;
|
||||
Node* UTILS_NULLABLE prev;
|
||||
Node* UTILS_NULLABLE next;
|
||||
|
||||
Node(Key&& k, T&& v, Node* UTILS_NULLABLE next)
|
||||
: key(std::move(k)),
|
||||
value(std::move(v)),
|
||||
prev(nullptr),
|
||||
next(next) {}
|
||||
};
|
||||
|
||||
void moveToFront(Node* UTILS_NONNULL node) {
|
||||
if (node == mHead) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, detach from list.
|
||||
if (UTILS_UNLIKELY(node == mTail)) {
|
||||
assert(node->prev != nullptr);
|
||||
mTail = node->prev;
|
||||
mTail->next = nullptr;
|
||||
} else {
|
||||
assert(node->prev != nullptr);
|
||||
assert(node->next != nullptr);
|
||||
node->prev->next = node->next;
|
||||
node->next->prev = node->prev;
|
||||
}
|
||||
|
||||
// Then attach to head.
|
||||
node->prev = nullptr;
|
||||
node->next = mHead;
|
||||
attach(node);
|
||||
}
|
||||
|
||||
// Attach node to front.
|
||||
void attach(Node* UTILS_NONNULL node) {
|
||||
if (UTILS_LIKELY(mHead != nullptr)) {
|
||||
mHead->prev = node;
|
||||
} else {
|
||||
assert(mTail == nullptr);
|
||||
mTail = node;
|
||||
}
|
||||
mHead = node;
|
||||
}
|
||||
|
||||
// Evict the least recently used node.
|
||||
template<typename F>
|
||||
void evict(F releaser) {
|
||||
Node* node = mTail;
|
||||
assert(node != nullptr);
|
||||
|
||||
if (UTILS_UNLIKELY(node == mHead)) {
|
||||
mTail = nullptr;
|
||||
mHead = nullptr;
|
||||
} else {
|
||||
// Move new tail node to mTail.
|
||||
assert(node->prev != nullptr);
|
||||
mTail = node->prev;
|
||||
mTail->next = nullptr;
|
||||
}
|
||||
|
||||
// Finally free the node.
|
||||
mMap.erase(node->key);
|
||||
releaser(std::move(node->value));
|
||||
mArena.destroy(node);
|
||||
}
|
||||
|
||||
size_type mCapacity;
|
||||
utils::Arena<utils::ObjectPoolAllocator<Node>, utils::LockingPolicy::NoLock> mArena;
|
||||
tsl::robin_map<MapKey, Node* UTILS_NONNULL, Hash, KeyEqual> mMap;
|
||||
Node* UTILS_NULLABLE mHead;
|
||||
Node* UTILS_NULLABLE mTail;
|
||||
};
|
||||
|
||||
} // namespace utils
|
||||
|
||||
#endif // TNT_UTILS_LRUCACHE_H
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <utils/Panic.h>
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/debug.h>
|
||||
#include <utils/LruCache.h>
|
||||
|
||||
#include <tsl/robin_map.h>
|
||||
|
||||
@@ -75,8 +76,6 @@ class RefCountedMap {
|
||||
T value;
|
||||
};
|
||||
|
||||
using Map = tsl::robin_map<Key, Entry, Hash>;
|
||||
|
||||
static constexpr TValue& deref(T& a) {
|
||||
if constexpr (refcountedmap::IsPointer<T>) {
|
||||
return *a;
|
||||
@@ -99,6 +98,9 @@ class RefCountedMap {
|
||||
"Attempted to get missing value";
|
||||
|
||||
public:
|
||||
explicit RefCountedMap(const char* UTILS_NONNULL name = "RefCountedMap", size_t lruCapacity = 0)
|
||||
: mLruCache(name, lruCapacity) {}
|
||||
|
||||
/** Acquire and return a value by key, initializing it with F if it doesn't exist.
|
||||
*
|
||||
* If F returns NullValue{}(), this indicates a failure to create the object. If T is a value
|
||||
@@ -111,12 +113,16 @@ public:
|
||||
it.value().referenceCount++;
|
||||
return &deref(it.value().value);
|
||||
}
|
||||
|
||||
if (std::optional<T> lruValue = mLruCache.pop(key, hash)) {
|
||||
return &insert(key, std::move(*lruValue));
|
||||
}
|
||||
|
||||
T r = factory();
|
||||
if (r == NullValue{}()) {
|
||||
return nullptr;
|
||||
}
|
||||
// TODO: how to use above computed hash here?
|
||||
return &deref(mMap.insert({ key, Entry{ 1, std::move(r) } }).first.value().value);
|
||||
return &insert(key, std::move(r));
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
@@ -138,6 +144,11 @@ public:
|
||||
it.value().referenceCount++;
|
||||
return &deref(it.value().value);
|
||||
}
|
||||
|
||||
if (std::optional<T> lruValue = mLruCache.pop(key, hash)) {
|
||||
return &insert(key, std::move(*lruValue));
|
||||
}
|
||||
|
||||
// TODO: how to use above computed hash here?
|
||||
mMap.insert({ key, Entry{ 1, NullValue{}() } });
|
||||
return nullptr;
|
||||
@@ -156,8 +167,14 @@ public:
|
||||
auto it = mMap.find(key, hash);
|
||||
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
|
||||
if (--it.value().referenceCount == 0) {
|
||||
if (it.value().value != NullValue{}()){
|
||||
releaser(deref(it.value().value));
|
||||
if (it.value().value != NullValue{}()) {
|
||||
if (mLruCache.capacity() > 0) {
|
||||
mLruCache.put(key, std::move(it.value().value), hash, [&releaser](T&& v) {
|
||||
releaser(deref(v));
|
||||
});
|
||||
} else {
|
||||
releaser(deref(it.value().value));
|
||||
}
|
||||
}
|
||||
// TODO: change to erase_fast
|
||||
mMap.erase(it);
|
||||
@@ -177,6 +194,9 @@ public:
|
||||
auto it = mMap.find(key, hash);
|
||||
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
|
||||
if (--it.value().referenceCount == 0) {
|
||||
if (mLruCache.capacity() > 0) {
|
||||
mLruCache.put(key, std::move(it.value().value), hash, [](T&&){});
|
||||
}
|
||||
// TODO: change to erase_fast
|
||||
mMap.erase(it);
|
||||
}
|
||||
@@ -217,7 +237,7 @@ public:
|
||||
* Panics if no entry found in map.
|
||||
*/
|
||||
TValue& get(KeyRef key, size_t hash) {
|
||||
auto it = mMap.find(key);
|
||||
auto it = mMap.find(key, hash);
|
||||
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
|
||||
FILAMENT_CHECK_PRECONDITION(it.value().value != NullValue{}())
|
||||
<< MISSING_VALUE_ERROR_STRING;
|
||||
@@ -236,13 +256,33 @@ public:
|
||||
|
||||
inline TValue const& get(KeyRef key) const noexcept { return get(key, Hash{}(key)); }
|
||||
|
||||
/** Clear the LRU cache, calling releaser on each item evicted. */
|
||||
template<typename F>
|
||||
void clearLruCache(F releaser) {
|
||||
mLruCache.clear([&releaser](T&& v) {
|
||||
releaser(deref(v));
|
||||
});
|
||||
}
|
||||
|
||||
/** Clear the LRU cache without calling any releasers. */
|
||||
void clearLruCache() {
|
||||
mLruCache.clear();
|
||||
}
|
||||
|
||||
/** Returns true if the map is empty. */
|
||||
inline bool empty() const noexcept { return mMap.empty(); }
|
||||
|
||||
private:
|
||||
Map mMap;
|
||||
tsl::robin_map<Key, Entry, Hash> mMap;
|
||||
utils::LruCache<Key, T, Hash> mLruCache;
|
||||
|
||||
TValue& insert(KeyRef key, T value) {
|
||||
// TODO: how to use computed hash here?
|
||||
auto it = mMap.insert({ key, Entry{ 1, std::move(value) } });
|
||||
return deref(it.first.value().value);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
} // utils
|
||||
|
||||
#endif // TNT_UTILS_REFCOUNTEDMAP_H
|
||||
|
||||
@@ -87,7 +87,7 @@ FreeList::Node* FreeList::init(void* begin, void* end,
|
||||
void* const p = pointermath::align(begin, alignment, extra);
|
||||
void* const n = pointermath::align(pointermath::add(p, elementSize), alignment, extra);
|
||||
assert_invariant(p >= begin && p < end);
|
||||
assert_invariant(n >= begin && n < end && n > p);
|
||||
assert_invariant(n >= begin && n <= end && n > p);
|
||||
|
||||
const size_t d = uintptr_t(n) - uintptr_t(p);
|
||||
const size_t num = (uintptr_t(end) - uintptr_t(p)) / d;
|
||||
@@ -128,7 +128,7 @@ AtomicFreeList::AtomicFreeList(void* begin, void* end,
|
||||
void* const p = pointermath::align(begin, alignment, extra);
|
||||
void* const n = pointermath::align(pointermath::add(p, elementSize), alignment, extra);
|
||||
assert_invariant(p >= begin && p < end);
|
||||
assert_invariant(n >= begin && n < end && n > p);
|
||||
assert_invariant(n >= begin && n <= end && n > p);
|
||||
|
||||
const size_t d = uintptr_t(n) - uintptr_t(p);
|
||||
const size_t num = (uintptr_t(end) - uintptr_t(p)) / d;
|
||||
|
||||
268
libs/utils/test/test_LruCache.cpp
Normal file
268
libs/utils/test/test_LruCache.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <utils/LruCache.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace utils;
|
||||
|
||||
TEST(LruCacheTest, BasicPutGet) {
|
||||
LruCache<int, std::string> cache("LruCacheTest", 3);
|
||||
|
||||
cache.put(1, "one", [](std::string&&) {});
|
||||
cache.put(2, "two", [](std::string&&) {});
|
||||
|
||||
auto* v1 = cache.get(1);
|
||||
ASSERT_NE(v1, nullptr);
|
||||
EXPECT_EQ(*v1, "one");
|
||||
|
||||
auto* v2 = cache.get(2);
|
||||
ASSERT_NE(v2, nullptr);
|
||||
EXPECT_EQ(*v2, "two");
|
||||
|
||||
EXPECT_EQ(cache.size(), 2);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, MissingKey) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
EXPECT_EQ(cache.get(999), nullptr);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, Eviction) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
cache.put(3, 30, releaser);
|
||||
|
||||
// Cache: 3(MRU), 2, 1(LRU)
|
||||
EXPECT_EQ(cache.size(), 3);
|
||||
EXPECT_TRUE(evicted.empty());
|
||||
|
||||
cache.put(4, 40, releaser);
|
||||
// Evicted 1. Cache: 4(MRU), 3, 2
|
||||
EXPECT_EQ(cache.size(), 3);
|
||||
ASSERT_EQ(evicted.size(), 1);
|
||||
EXPECT_EQ(evicted[0], 10);
|
||||
|
||||
// Verify contents
|
||||
EXPECT_NE(cache.get(2), nullptr);
|
||||
EXPECT_NE(cache.get(3), nullptr);
|
||||
EXPECT_NE(cache.get(4), nullptr);
|
||||
EXPECT_EQ(cache.get(1), nullptr);
|
||||
|
||||
// Accessing 2 makes it MRU. Cache: 2(MRU), 4, 3(LRU)
|
||||
cache.get(2);
|
||||
|
||||
cache.put(5, 50, releaser);
|
||||
// Should evict 3.
|
||||
ASSERT_EQ(evicted.size(), 2);
|
||||
EXPECT_EQ(evicted[1], 30);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, UpdatePromotesToMRU) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
auto releaser = [](int&&) {};
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
cache.put(3, 30, releaser);
|
||||
// Order: 3, 2, 1
|
||||
|
||||
cache.put(1, 11, releaser);
|
||||
// Order: 1, 3, 2
|
||||
|
||||
cache.put(4, 40, releaser);
|
||||
// Order: 4, 1, 3, 2 (evicted)
|
||||
|
||||
// Check if 1 exists
|
||||
auto* v1 = cache.get(1);
|
||||
ASSERT_NE(v1, nullptr);
|
||||
EXPECT_EQ(*v1, 11);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, GetPromotesToMRU) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
cache.put(3, 30, releaser);
|
||||
// 3, 2, 1
|
||||
|
||||
cache.get(1);
|
||||
// 1, 3, 2
|
||||
|
||||
cache.put(4, 40, releaser);
|
||||
// Evicts 2.
|
||||
ASSERT_EQ(evicted.size(), 1);
|
||||
EXPECT_EQ(evicted[0], 20);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, MoveToFrontFromTail) {
|
||||
// This specific test targets the potential bug where moving from tail doesn't update tail.
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
cache.put(3, 30, releaser);
|
||||
// State: 3 (MRU) -> 2 -> 1 (LRU, Tail)
|
||||
|
||||
// Move 1 to front.
|
||||
cache.get(1);
|
||||
// Expected State: 1 (MRU) -> 3 -> 2 (LRU, Tail)
|
||||
|
||||
// Add 4. Should evict 2.
|
||||
cache.put(4, 40, releaser);
|
||||
|
||||
ASSERT_EQ(evicted.size(), 1);
|
||||
EXPECT_EQ(evicted[0], 20);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, MoveToFrontFromMiddle) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
cache.put(3, 30, releaser);
|
||||
// 3, 2, 1
|
||||
|
||||
cache.get(2);
|
||||
// 2, 3, 1
|
||||
|
||||
cache.put(4, 40, releaser);
|
||||
// Evicts 1.
|
||||
ASSERT_EQ(evicted.size(), 1);
|
||||
EXPECT_EQ(evicted[0], 10);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, CapacityOne) {
|
||||
LruCache<int, int> cache("LruCacheTest", 1);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
EXPECT_NE(cache.get(1), nullptr);
|
||||
EXPECT_EQ(*cache.get(1), 10);
|
||||
|
||||
cache.put(2, 20, releaser);
|
||||
ASSERT_EQ(evicted.size(), 1);
|
||||
EXPECT_EQ(evicted[0], 10);
|
||||
EXPECT_NE(cache.get(2), nullptr);
|
||||
EXPECT_EQ(*cache.get(2), 20);
|
||||
}
|
||||
|
||||
#ifdef GTEST_HAS_DEATH_TEST
|
||||
TEST(LruCacheTest, CapacityZero) {
|
||||
LruCache<int, int> cache("LruCacheTest", 0);
|
||||
EXPECT_EQ(cache.capacity(), 0);
|
||||
EXPECT_EQ(cache.size(), 0);
|
||||
EXPECT_DEATH(cache.put(1, 1, [](int&&){}), "");
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(LruCacheTest, Clear) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
std::vector<int> evicted;
|
||||
auto releaser = [&](int&& v) { evicted.push_back(v); };
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
|
||||
EXPECT_EQ(cache.size(), 2);
|
||||
|
||||
cache.clear(releaser);
|
||||
|
||||
EXPECT_EQ(cache.size(), 0);
|
||||
ASSERT_EQ(evicted.size(), 2);
|
||||
// Implementation traverses mHead -> mTail (MRU to LRU).
|
||||
EXPECT_EQ(evicted[0], 20); // MRU
|
||||
EXPECT_EQ(evicted[1], 10); // LRU
|
||||
|
||||
// We should now be able to refill the cache.
|
||||
cache.put(3, 30, releaser);
|
||||
cache.put(4, 40, releaser);
|
||||
|
||||
EXPECT_EQ(cache.size(), 2);
|
||||
EXPECT_NE(cache.get(3), nullptr);
|
||||
EXPECT_NE(cache.get(4), nullptr);
|
||||
}
|
||||
|
||||
TEST(LruCacheTest, Pop) {
|
||||
LruCache<int, int> cache("LruCacheTest", 3);
|
||||
auto releaser = [](int&&) {};
|
||||
|
||||
cache.put(1, 10, releaser);
|
||||
cache.put(2, 20, releaser);
|
||||
|
||||
std::optional<int> val1 = cache.pop(1);
|
||||
EXPECT_TRUE(val1.has_value());
|
||||
EXPECT_EQ(*val1, 10);
|
||||
EXPECT_EQ(cache.size(), 1);
|
||||
EXPECT_EQ(cache.get(1), nullptr);
|
||||
EXPECT_NE(cache.get(2), nullptr);
|
||||
|
||||
std::optional<int> val2 = cache.pop(2);
|
||||
EXPECT_TRUE(val2.has_value());
|
||||
EXPECT_EQ(*val2, 20);
|
||||
EXPECT_EQ(cache.size(), 0);
|
||||
}
|
||||
|
||||
/* Large key tests. */
|
||||
|
||||
struct LargeKey {
|
||||
long long a, b;
|
||||
bool operator==(LargeKey const& other) const {
|
||||
return a == other.a && b == other.b;
|
||||
}
|
||||
};
|
||||
|
||||
struct LargeKeyHash {
|
||||
size_t operator()(LargeKey const& k) const {
|
||||
return std::hash<long long>{}(k.a) ^ std::hash<long long>{}(k.b);
|
||||
}
|
||||
};
|
||||
|
||||
TEST(LruCacheTest, LargeKey) {
|
||||
static_assert(sizeof(LargeKey) > sizeof(void*), "LargeKey must be larger than pointer");
|
||||
LruCache<LargeKey, int, LargeKeyHash> cache("LruCacheTest", 2);
|
||||
|
||||
LargeKey k1{1, 1};
|
||||
LargeKey k2{2, 2};
|
||||
LargeKey k3{3, 3};
|
||||
|
||||
cache.put(k1, 100, [](int&&){});
|
||||
cache.put(k2, 200, [](int&&){});
|
||||
|
||||
EXPECT_NE(cache.get(k1), nullptr);
|
||||
EXPECT_EQ(*cache.get(k1), 100);
|
||||
|
||||
// Evict k2 (LRU because k1 was accessed)
|
||||
cache.put(k3, 300, [](int&&){});
|
||||
|
||||
EXPECT_NE(cache.get(k3), nullptr); // k3 present
|
||||
}
|
||||
@@ -261,3 +261,62 @@ TEST(RefCountedMapTest, SmartPointerType_PanicsIfGetsNullValue) {
|
||||
ASSERT_DEATH(map.get(1), "");
|
||||
}
|
||||
#endif // GTEST_HAS_DEATH_TEST
|
||||
|
||||
TEST(RefCountedMapTest, LruRecycling) {
|
||||
RefCountedMap<KeyType, SmartPointerType> map("RefCountedMapTest", 1);
|
||||
bool factoryCalled = false;
|
||||
auto factory = [&]() {
|
||||
factoryCalled = true;
|
||||
return std::make_unique<size_t>(100);
|
||||
};
|
||||
|
||||
// 1. Acquire K1. Ref=1.
|
||||
ValueType* v1 = map.acquire(1, factory);
|
||||
ASSERT_NE(v1, nullptr);
|
||||
EXPECT_EQ(*v1, 100);
|
||||
EXPECT_TRUE(factoryCalled);
|
||||
|
||||
// 2. Release K1. Ref=0. Should move to LRU.
|
||||
map.release(1);
|
||||
// map.empty() checks mMap. mMap should be empty.
|
||||
EXPECT_TRUE(map.empty());
|
||||
|
||||
// 3. Acquire K1 again. Should come from LRU.
|
||||
factoryCalled = false;
|
||||
ValueType* v2 = map.acquire(1, factory);
|
||||
ASSERT_NE(v2, nullptr);
|
||||
EXPECT_EQ(*v2, 100);
|
||||
// Factory should NOT be called.
|
||||
EXPECT_FALSE(factoryCalled);
|
||||
// The underlying pointer (ValueType*) should be the same.
|
||||
EXPECT_EQ(v1, v2);
|
||||
|
||||
// 4. Release K1.
|
||||
map.release(1);
|
||||
}
|
||||
|
||||
TEST(RefCountedMapTest, ClearLruCache) {
|
||||
RefCountedMap<KeyType, ValueType> map("RefCountedMapTest", 2);
|
||||
int destroyed = 0;
|
||||
auto releaser = [&](ValueType& v) { destroyed++; };
|
||||
|
||||
// Acquire and release two items to fill LRU
|
||||
map.acquire(1, []{ return 10; });
|
||||
map.release(1, releaser);
|
||||
map.acquire(2, []{ return 20; });
|
||||
map.release(2, releaser);
|
||||
|
||||
// LRU size 2. Destoyed 0.
|
||||
EXPECT_EQ(destroyed, 0);
|
||||
|
||||
map.clearLruCache(releaser);
|
||||
|
||||
EXPECT_EQ(destroyed, 2);
|
||||
|
||||
// Check they are gone (revival fails)
|
||||
bool factoryCalled = false;
|
||||
map.acquire(1, [&]{ factoryCalled = true; return 11; });
|
||||
EXPECT_TRUE(factoryCalled);
|
||||
|
||||
map.release(1, releaser);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user