From 8a3c48fef188382ebab7ecff614d00dbe0b5449c Mon Sep 17 00:00:00 2001 From: Eliza <4576666+elizagamedev@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:04:16 +0900 Subject: [PATCH] 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 --- filament/src/MaterialCache.cpp | 13 + filament/src/MaterialCache.h | 5 + filament/src/details/Engine.cpp | 3 + libs/utils/CMakeLists.txt | 2 + libs/utils/include/utils/LruCache.h | 322 +++++++++++++++++++++++ libs/utils/include/utils/RefCountedMap.h | 58 +++- libs/utils/src/Allocator.cpp | 4 +- libs/utils/test/test_LruCache.cpp | 268 +++++++++++++++++++ libs/utils/test/test_RefCountedMap.cpp | 59 +++++ 9 files changed, 723 insertions(+), 11 deletions(-) create mode 100644 libs/utils/include/utils/LruCache.h create mode 100644 libs/utils/test/test_LruCache.cpp diff --git a/filament/src/MaterialCache.cpp b/filament/src/MaterialCache.cpp index 7fadc1e673..f8289cc4d0 100644 --- a/filament/src/MaterialCache.cpp +++ b/filament/src/MaterialCache.cpp @@ -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& 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 parser = MaterialDefinition::createParser(engine.getBackend(), diff --git a/filament/src/MaterialCache.h b/filament/src/MaterialCache.h index 08b3f16dae..952a1283fa 100644 --- a/filament/src/MaterialCache.h +++ b/filament/src/MaterialCache.h @@ -56,8 +56,13 @@ public: using ProgramCache = utils::RefCountedMap>; + 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; } diff --git a/filament/src/details/Engine.cpp b/filament/src/details/Engine.cpp index 10027fac3a..cdf7f584b6 100644 --- a/filament/src/details/Engine.cpp +++ b/filament/src/details/Engine.cpp @@ -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(); /* diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index a3eea97958..70815fc7eb 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -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 diff --git a/libs/utils/include/utils/LruCache.h b/libs/utils/include/utils/LruCache.h new file mode 100644 index 0000000000..2c0a24197f --- /dev/null +++ b/libs/utils/include/utils/LruCache.h @@ -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 +#include + +#include + +#include +#include +#include +#include +#include + +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 KeyEqual = std::equal_to> +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, 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 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 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 + 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(std::move(key), std::move(value), mHead); + assert(node != nullptr); + attach(node); + + mMap.emplace(node->key, node); + + return node->value; + } + + template + 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 + 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 + 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::LockingPolicy::NoLock> mArena; + tsl::robin_map mMap; + Node* UTILS_NULLABLE mHead; + Node* UTILS_NULLABLE mTail; +}; + +} // namespace utils + +#endif // TNT_UTILS_LRUCACHE_H diff --git a/libs/utils/include/utils/RefCountedMap.h b/libs/utils/include/utils/RefCountedMap.h index 5fc0b85f5c..05163340ae 100644 --- a/libs/utils/include/utils/RefCountedMap.h +++ b/libs/utils/include/utils/RefCountedMap.h @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -75,8 +76,6 @@ class RefCountedMap { T value; }; - using Map = tsl::robin_map; - static constexpr TValue& deref(T& a) { if constexpr (refcountedmap::IsPointer) { 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 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 @@ -138,6 +144,11 @@ public: it.value().referenceCount++; return &deref(it.value().value); } + + if (std::optional 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 + 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 mMap; + utils::LruCache 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 diff --git a/libs/utils/src/Allocator.cpp b/libs/utils/src/Allocator.cpp index fd6e594569..7552264b69 100644 --- a/libs/utils/src/Allocator.cpp +++ b/libs/utils/src/Allocator.cpp @@ -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; diff --git a/libs/utils/test/test_LruCache.cpp b/libs/utils/test/test_LruCache.cpp new file mode 100644 index 0000000000..47b58ce032 --- /dev/null +++ b/libs/utils/test/test_LruCache.cpp @@ -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 + +#include + +#include +#include + +using namespace utils; + +TEST(LruCacheTest, BasicPutGet) { + LruCache 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 cache("LruCacheTest", 3); + EXPECT_EQ(cache.get(999), nullptr); +} + +TEST(LruCacheTest, Eviction) { + LruCache cache("LruCacheTest", 3); + std::vector 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 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 cache("LruCacheTest", 3); + std::vector 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 cache("LruCacheTest", 3); + std::vector 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 cache("LruCacheTest", 3); + std::vector 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 cache("LruCacheTest", 1); + std::vector 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 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 cache("LruCacheTest", 3); + std::vector 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 cache("LruCacheTest", 3); + auto releaser = [](int&&) {}; + + cache.put(1, 10, releaser); + cache.put(2, 20, releaser); + + std::optional 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 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{}(k.a) ^ std::hash{}(k.b); + } +}; + +TEST(LruCacheTest, LargeKey) { + static_assert(sizeof(LargeKey) > sizeof(void*), "LargeKey must be larger than pointer"); + LruCache 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 +} diff --git a/libs/utils/test/test_RefCountedMap.cpp b/libs/utils/test/test_RefCountedMap.cpp index 8340412816..adc7979e12 100644 --- a/libs/utils/test/test_RefCountedMap.cpp +++ b/libs/utils/test/test_RefCountedMap.cpp @@ -261,3 +261,62 @@ TEST(RefCountedMapTest, SmartPointerType_PanicsIfGetsNullValue) { ASSERT_DEATH(map.get(1), ""); } #endif // GTEST_HAS_DEATH_TEST + +TEST(RefCountedMapTest, LruRecycling) { + RefCountedMap map("RefCountedMapTest", 1); + bool factoryCalled = false; + auto factory = [&]() { + factoryCalled = true; + return std::make_unique(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 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); +}