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:
Eliza
2026-03-05 15:04:16 +09:00
committed by GitHub
parent 7f6b9bb144
commit 8a3c48fef1
9 changed files with 723 additions and 11 deletions

View File

@@ -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(),

View File

@@ -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;
}

View File

@@ -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();
/*

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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;

View 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
}

View File

@@ -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);
}