From e9c17a7c3d048c2fbc2d67c03bd62702c75f6945 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Sun, 10 Mar 2024 22:21:18 +0200 Subject: [PATCH] change cache design to be shared --- loader/src/server/Server.hpp | 166 ++++++++++++++++++++----------- loader/src/ui/GeodeUI.cpp | 4 +- loader/src/ui/mods/ModsLayer.cpp | 1 + 3 files changed, 111 insertions(+), 60 deletions(-) diff --git a/loader/src/server/Server.hpp b/loader/src/server/Server.hpp index c0f4b32a..c6397c65 100644 --- a/loader/src/server/Server.hpp +++ b/loader/src/server/Server.hpp @@ -99,76 +99,128 @@ namespace server { ServerPromise<ByteVector> getModLogo(std::string const& id); // Caching for server endpoints - namespace impl_cache { + namespace detail { template <class R, class Q> struct ExtractServerReqParams { using Result = R; using Query = Q; ExtractServerReqParams(ServerPromise<R>(*)(Q const&)) {} }; + } - template <auto F> - class ServerResultCache final { - public: - using Extract = decltype(ExtractServerReqParams(F)); - using Result = Extract::Result; - using Query = Extract::Query; - using Cached = std::variant<ServerPromise<Result>, Result>; + template <auto F> + class ServerResultCache final { + public: + using Extract = decltype(detail::ExtractServerReqParams(F)); + using Result = Extract::Result; + using Query = Extract::Query; + using Cached = std::variant<ServerPromise<Result>, Result>; - private: - std::mutex m_cachedMutex; - std::map<Query, Result> m_cached; - - ServerPromise<Result> fetch(Query const& query) { - return ServerPromise<Result>([this, query = Query(query)](auto resolve, auto reject, auto progress, auto cancelled) { - F(Query(query)) - .then([this, resolve, query = std::move(query)](auto res) { - std::unique_lock lock(m_cachedMutex); - m_cached[std::move(query)] = res; - lock.unlock(); - - resolve(res); - }) - .expect([reject](auto err) { - reject(err); - }) - .progress([progress](auto prog) { - progress(prog); - }) - .link(cancelled); - }); - } + private: + class Cache final { + std::mutex m_mutex; + // I know this looks like a goofy choice over just + // `std::unordered_map`, but hear me out: + // + // This needs preserved insertion order (so shrinking the cache + // to match size limits doesn't just have to erase random + // elements) + // + // If this used a map for values and another vector for storing + // insertion order, it would have a pretty big memory footprint + // (two copies of Query, one for order, one for map + two heap + // allocations on top of that) + // + // In addition, it would be a bad idea to have a cache of 1000s + // of items in any case (since that would likely take up a ton + // of memory, which we want to avoid since it's likely many + // crashes with the old index were due to too much memory + // usage) + // + // Linear searching a vector of at most a couple dozen items is + // lightning-fast (🚀), and besides the main performance benefit + // comes from the lack of a web request - not how many extra + // milliseconds we can squeeze out of a map access + std::vector<std::pair<Query, Result>> m_values; public: - ServerPromise<Result> get(Query const& query) { - std::unique_lock lock(m_cachedMutex); - // Return cached value if there is one and the query matches - if (m_cached.contains(query)) { - auto cached = m_cached.at(query); - lock.unlock(); - return ServerPromise<Result>([cached = std::move(cached)](auto resolve, auto) { - resolve(std::move(cached)); - }); + std::optional<Result> get(Query const& query) { + std::unique_lock _(m_mutex); + // mfw no std::optional::map + if (auto found = ranges::find(m_values, [&query](auto const& q) { return q.first == query; })) { + return found->second; } - lock.unlock(); - return this->fetch(std::move(query)); + return std::nullopt; } - - ServerPromise<Result> refetch(Query const& query) { - // Clear cache for this query only - std::unique_lock lock (m_cachedMutex); - m_cached.erase(query); - lock.unlock(); - - // Fetch new value - return this->fetch(std::move(query)); + void add(Query&& query, Result&& result) { + std::unique_lock _(m_mutex); + m_values.emplace_back(std::make_pair(std::move(query), std::move(result))); } - - // Clear all caches - void invalidateAll() { - std::unique_lock _(m_cachedMutex); - m_cached.clear(); + void remove(Query const& query) { + std::unique_lock _(m_mutex); + ranges::remove(m_values, [&query](auto const& q) { return q.first == query; }); + } + void shrink(size_t size) { + std::unique_lock _(m_mutex); + size_t removeCount = size < m_values.size() ? m_values.size() - size : 0; + m_values.erase(m_values.begin(), m_values.begin() + removeCount); + } + void clear() { + std::unique_lock _(m_mutex); + m_values.clear(); } }; - } + + Cache m_cache; + // todo: loader setting to customize this + static constexpr size_t CACHE_SIZE_LIMIT = 20; + + ServerPromise<Result> fetch(Query const& query) { + return ServerPromise<Result>([this, query = Query(query)](auto resolve, auto reject, auto progress, auto cancelled) { + F(Query(query)) + .then([this, resolve, query = std::move(query)](auto res) { + m_cache.add(Query(query), Result(res)); + resolve(res); + }) + .expect([reject](auto err) { + reject(err); + }) + .progress([progress](auto prog) { + progress(prog); + }) + .link(cancelled); + }); + } + + public: + static ServerResultCache<F>& shared() { + static auto inst = ServerResultCache<F>(); + return inst; + } + + ServerPromise<Result> get(Query const& query) { + // Return cached value if there is one and the query matches + if (auto cached = m_cache.get(query)) { + return ServerPromise<Result>([cached = std::move(cached).value()](auto resolve, auto) { + resolve(std::move(cached)); + }); + } + // Shrink to fit size limit + 1 new item + m_cache.shrink(CACHE_SIZE_LIMIT - 1); + return this->fetch(std::move(query)); + } + + ServerPromise<Result> refetch(Query const& query) { + // Clear cache for this query only + m_cache.remove(query); + + // Fetch new value + return this->fetch(std::move(query)); + } + + // Clear all caches + void invalidateAll() { + m_cache.clear(); + } + }; } diff --git a/loader/src/ui/GeodeUI.cpp b/loader/src/ui/GeodeUI.cpp index 93f50428..3b471d4e 100644 --- a/loader/src/ui/GeodeUI.cpp +++ b/loader/src/ui/GeodeUI.cpp @@ -70,8 +70,6 @@ protected: std::string m_modID; CCNode* m_sprite = nullptr; EventListener<PromiseEventFilter<ByteVector, server::ServerError>> m_listener; - // todo: use shared cache once impl'd - static inline server::impl_cache::ServerResultCache<&server::getModLogo> s_cache = {}; bool init(std::string const& id, bool fetch) { if (!CCNode::init()) @@ -95,7 +93,7 @@ protected: this->setSprite(CCSprite::create("loadingCircle.png")); static_cast<CCSprite*>(m_sprite)->setBlendFunc({ GL_ONE, GL_ONE }); m_sprite->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); - m_listener.setFilter(s_cache.get(id).listen()); + m_listener.setFilter(server::ServerResultCache<&server::getModLogo>::shared().get(id).listen()); } return true; diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index 34bd0723..65924c22 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -264,6 +264,7 @@ void ModsLayer::onRefreshList(CCObject*) { void ModsLayer::onBack(CCObject*) { CCDirector::get()->replaceScene(CCTransitionFade::create(.5f, MenuLayer::scene(false))); + server::ServerResultCache<&server::getModLogo>::shared().invalidateAll(); } void ModsLayer::onGoToPage(CCObject*) {