Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions category/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ add_library(
"tl_tid.c"
"tl_tid.h"
"detail/start_lifetime_as_polyfill.hpp"
"lru/cache_stats.hpp"
"lru/lru_cache.hpp"
"lru/static_lru_cache.hpp"
"mem/batch_mem_pool.hpp"
Expand Down
111 changes: 111 additions & 0 deletions category/core/lru/cache_stats.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (C) 2025-26 Category Labs, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#pragma once

#include <category/core/config.hpp>

#include <atomic>
#include <cstdint>

MONAD_NAMESPACE_BEGIN

struct CacheStatsSnapshot
{
uint64_t hits{0};
uint64_t misses{0};
uint64_t evictions{0}; // entries dropped to stay within the cache's bound
};

// Counters only ever increase, so a negative difference means the two
// snapshots came from different caches. Saturate rather than wrap: a window
// reading zero is a legible "no activity", where a wrap prints ~1.8e19 into
// the block metrics line and overflows the percentage back to a plausible 0%.
constexpr uint64_t
monotonic_delta(uint64_t const now, uint64_t const base) noexcept
{
return now > base ? now - base : 0;
}

constexpr CacheStatsSnapshot
operator-(CacheStatsSnapshot const &a, CacheStatsSnapshot const &b) noexcept
{
return {
.hits = monotonic_delta(a.hits, b.hits),
.misses = monotonic_delta(a.misses, b.misses),
.evictions = monotonic_delta(a.evictions, b.evictions)};
}

// Reports cache activity over a window — a block, for the metrics log line —
// without disturbing the underlying counters, which stay cumulative. Before
// the first reset the window covers the whole history.
//
// The baseline is not atomic: reset() and since() must run on one thread, and
// both snapshots must come from the same cache.
class CacheStatsWindow
{
CacheStatsSnapshot baseline_{};

public:
void reset(CacheStatsSnapshot const &now) noexcept
{
baseline_ = now;
}

CacheStatsSnapshot since(CacheStatsSnapshot const &now) const noexcept
{
return now - baseline_;
}
};

// Hit/miss/eviction counts for the life of the cache object. Reads are
// non-destructive, so independent readers do not consume each other's counts.
//
// The counters are relaxed because they are statistics, not synchronization —
// they order nothing and a reader tolerates a slightly stale total. The three
// loads in snapshot() are independent, so a snapshot is not a single instant;
// each counter is monotonic, which is all the window subtraction needs.
class CacheStats
{
std::atomic<uint64_t> hits_{0};
std::atomic<uint64_t> misses_{0};
std::atomic<uint64_t> evictions_{0};

public:
void record_hit() noexcept
{
hits_.fetch_add(1, std::memory_order_relaxed);
}

void record_miss() noexcept
{
misses_.fetch_add(1, std::memory_order_relaxed);
}

void record_eviction() noexcept
{
evictions_.fetch_add(1, std::memory_order_relaxed);
}

CacheStatsSnapshot snapshot() const noexcept
{
return {
.hits = hits_.load(std::memory_order_relaxed),
.misses = misses_.load(std::memory_order_relaxed),
.evictions = evictions_.load(std::memory_order_relaxed)};
}
};

MONAD_NAMESPACE_END
117 changes: 7 additions & 110 deletions category/core/lru/lru_cache.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#pragma once

#include <category/core/assert.h>
#include <category/core/lru/cache_stats.hpp>
#include <category/core/mem/batch_mem_pool.hpp>
#include <category/core/synchronization/spin_lock.hpp>

Expand Down Expand Up @@ -52,23 +53,7 @@ class LruCache
Mutex mutex_;
HashMap hmap_;
Pool pool_;

/// STATS MACROS
#ifdef MONAD_LRU_CACHE_STATS
#define STATS_EVENT_EVICT() stats_.event_evict()
#define STATS_EVENT_FIND_HIT() stats_.event_find_hit()
#define STATS_EVENT_FIND_MISS() stats_.event_find_miss()
#define STATS_EVENT_INSERT_FOUND() stats_.event_insert_found()
#define STATS_EVENT_INSERT_NEW() stats_.event_insert_new()
#define STATS_EVENT_UPDATE_LRU() stats_.event_update_lru()
#else
#define STATS_EVENT_EVICT()
#define STATS_EVENT_FIND_HIT()
#define STATS_EVENT_FIND_MISS()
#define STATS_EVENT_INSERT_FOUND()
#define STATS_EVENT_INSERT_NEW()
#define STATS_EVENT_UPDATE_LRU()
#endif
CacheStats stats_;

public:
using ConstAccessor = HashMap::const_accessor;
Expand All @@ -92,10 +77,10 @@ class LruCache
bool find(ConstAccessor &acc, Key const &key)
{
if (!hmap_.find(acc, key)) {
STATS_EVENT_FIND_MISS();
stats_.record_miss();
return false;
}
STATS_EVENT_FIND_HIT();
stats_.record_hit();
ListNode *const node = acc->second.node_;
try_update_lru(node);
return true;
Expand All @@ -106,7 +91,6 @@ class LruCache
Accessor acc;
HashMapKeyValue const hmkv(key, HashMapValue(value, nullptr));
if (!hmap_.insert(acc, hmkv)) {
STATS_EVENT_INSERT_FOUND();
acc->second.value_ = value;
ListNode *const node = acc->second.node_;
try_update_lru(node);
Expand Down Expand Up @@ -136,7 +120,6 @@ class LruCache
{
if (node->check_lru_time()) {
std::unique_lock const l(mutex_);
STATS_EVENT_UPDATE_LRU();
lru_.update_lru(node);
}
}
Expand All @@ -147,7 +130,6 @@ class LruCache
bool const evicted = (sz >= max_size_) && evict();
{
std::unique_lock const l(mutex_);
STATS_EVENT_INSERT_NEW();
lru_.push_front(node);
}
if (!evicted) {
Expand All @@ -171,12 +153,12 @@ class LruCache
ListNode *target;
{
std::unique_lock const l(mutex_);
STATS_EVENT_EVICT();
target = lru_.evict();
}
if (!target) {
return false;
}
stats_.record_eviction();
Accessor acc;
bool const found = hmap_.find(acc, target->key_);
MONAD_ASSERT(found);
Expand Down Expand Up @@ -306,96 +288,11 @@ class LruCache
}
}; /// HashMapValue

/// STATS
#undef STATS_EVENT_EVICT
#undef STATS_EVENT_FIND_HIT
#undef STATS_EVENT_FIND_MISS
#undef STATS_EVENT_INSERT_FOUND
#undef STATS_EVENT_INSERT_NEW
#undef STATS_EVENT_UPDATE_LRU

public:
std::string print_stats()
CacheStatsSnapshot stats() const noexcept
{
std::string str =
std::format("{:8}", size_.load(std::memory_order_acquire));
#ifdef MONAD_LRU_CACHE_STATS
str += " / " + stats_.print_stats();
#endif
return str;
return stats_.snapshot();
}

private:
#ifdef MONAD_LRU_CACHE_STATS
/// CacheStats
struct CacheStats
{
std::atomic<uint64_t> n_find_hit_{0};
std::atomic<uint64_t> n_find_miss_{0};
std::atomic<uint64_t> n_insert_found_{0};
uint64_t n_insert_new_{0};
uint64_t n_evict_{0};
uint64_t n_update_lru_{0};

void event_find_hit()
{
n_find_hit_.fetch_add(1, std::memory_order_release);
}

void event_find_miss()
{
n_find_miss_.fetch_add(1, std::memory_order_release);
}

void event_insert_found()
{
n_insert_found_.fetch_add(1, std::memory_order_release);
}

void event_insert_new()
{
++n_insert_new_;
}

void event_evict()
{
++n_evict_;
}

void event_update_lru()
{
++n_update_lru_;
}

void clear_stats()
{
// Not called concurrently with cache operations.
n_find_hit_.store(0, std::memory_order_release);
n_find_miss_.store(0, std::memory_order_release);
n_insert_found_.store(0, std::memory_order_release);
n_insert_new_ = 0;
n_evict_ = 0;
n_update_lru_ = 0;
}

std::string print_stats()
{
std::string str = std::format(
"{:6} {:6} - {:6} {:6} - {:6} - {:6}",
n_find_hit_.load(std::memory_order_acquire),
n_find_miss_.load(std::memory_order_acquire),
n_insert_found_.load(std::memory_order_acquire),
n_insert_new_,
n_evict_,
n_update_lru_);
clear_stats();
return str;
}
}; /// CacheStats

CacheStats stats_;
#endif /// MONAD_LRU_CACHE_STATS

}; /// LruCache

MONAD_NAMESPACE_END
Loading
Loading