%% Copyright (c) 2026 Benoit Chesneau. Licensed under the MIT License. %% See the LICENSE file at the project root. -module(erllama_cache_counters). -moduledoc """ Cache subsystem operational counters. A single `atomics` array (one slot per metric) lives in `persistent_term` keyed by this module. Hot paths bump slots via `incr/1,2`; readers take a snapshot via `snapshot/0`. Slot indices are defined in `include/erllama_cache.hrl` as `?C_*` macros so callers can pass the symbolic constant rather than a magic integer. Adapter integrations (Prometheus, statsd, OpenTelemetry, etc.) read `snapshot/0` periodically; this module does not depend on any external metrics framework. """. -include("erllama_cache.hrl"). -export([ init/0, incr/1, incr/2, add/2, get/1, snapshot/0, reset/0 ]). -define(ARR_KEY, {?MODULE, atomics}). %% ============================================================================= %% Public API %% ============================================================================= %% Idempotent: subsequent calls no-op (the atomics array survives %% as long as persistent_term holds it). -spec init() -> ok. init() -> case persistent_term:get(?ARR_KEY, undefined) of undefined -> A = atomics:new(?C_NSLOTS, [{signed, false}]), persistent_term:put(?ARR_KEY, A), ok; _Existing -> ok end. -spec incr(pos_integer()) -> ok. incr(Slot) -> add(Slot, 1). -spec incr(pos_integer(), non_neg_integer()) -> ok. incr(Slot, N) -> add(Slot, N). -spec add(pos_integer(), non_neg_integer()) -> ok. add(Slot, N) -> case persistent_term:get(?ARR_KEY, undefined) of undefined -> ok; A -> atomics:add(A, Slot, N) end. -spec get(pos_integer()) -> non_neg_integer(). get(Slot) -> case persistent_term:get(?ARR_KEY, undefined) of undefined -> 0; A -> atomics:get(A, Slot) end. -spec snapshot() -> #{atom() => non_neg_integer()}. snapshot() -> case persistent_term:get(?ARR_KEY, undefined) of undefined -> #{}; A -> maps:from_list([ {slot_name(N), atomics:get(A, N)} || N <- lists:seq(1, ?C_NSLOTS) ]) end. -spec reset() -> ok. reset() -> case persistent_term:get(?ARR_KEY, undefined) of undefined -> ok; A -> lists:foreach(fun(N) -> atomics:put(A, N, 0) end, lists:seq(1, ?C_NSLOTS)) end. %% ============================================================================= %% Internal: slot -> atom name mapping %% ============================================================================= slot_name(?C_HITS_EXACT) -> hits_exact; slot_name(?C_HITS_RESUME) -> hits_resume; slot_name(?C_MISSES) -> misses; slot_name(?C_SAVES_COLD) -> saves_cold; slot_name(?C_SAVES_CONTINUED) -> saves_continued; slot_name(?C_SAVES_FINISH) -> saves_finish; slot_name(?C_SAVES_EVICT) -> saves_evict; slot_name(?C_SAVES_SHUTDOWN) -> saves_shutdown; slot_name(?C_EVICTIONS) -> evictions; slot_name(?C_CORRUPT_FILES) -> corrupt_files; slot_name(?C_PACK_TOTAL_NS) -> pack_total_ns; slot_name(?C_LOAD_TOTAL_NS) -> load_total_ns; slot_name(?C_BYTES_RAM) -> bytes_ram; slot_name(?C_BYTES_RAMFILE) -> bytes_ramfile; slot_name(?C_BYTES_DISK) -> bytes_disk; slot_name(?C_DUPLICATE_DROPPED) -> duplicate_dropped; slot_name(?C_HITS_LONGEST_PREFIX) -> hits_longest_prefix; slot_name(?C_LONGEST_PREFIX_PROBES) -> longest_prefix_probes; slot_name(?C_LONGEST_PREFIX_NS) -> longest_prefix_ns; slot_name(?C_SAVES_DROPPED) -> saves_dropped.