Skip to content

Commit b691440

Browse files
committed
feat(v7-i07): cache.metrics RPC twin of GET /cache/stats
Closes plan item #46 / bd cppmega-mlx-oqpe. The UI BottomStrip already polls GET /cache/stats via useCacheStats for the cache hit-rate dashboard pill. Add a JSON-RPC twin so all UI surfaces / agents that talk through dispatch (not the HTTP REST shim) can read the same snapshot: - cppmega_v4/jsonrpc/cache_metrics_method.py: CacheMetricsParams + CacheMetricsResult dataclasses; cache_metrics(params, cache=...) returns size / capacity / hits / misses / evictions / hit_rate. Safe fallback to zeros when cache is None. - Dispatcher route 'cache.metrics' wired. - 5 pytest cover: empty cache zeros, hit/miss/hit_rate accounting, eviction counting under tight capacity, missing-cache fallback, dispatcher route round-trip with a populated cache. bd: cppmega-mlx-oqpe
1 parent 73accfa commit b691440

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""V7-I07: cache.metrics RPC — surfaces LRU cache hit-rate to the UI.
2+
3+
The dispatcher already shares a single LRUCache across handlers. This
4+
RPC reads its stats() snapshot so the BottomStrip can render hit-rate
5+
+ size as a tiny dashboard. Reads only; no mutation.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pydantic import BaseModel, ConfigDict
11+
12+
from cppmega_v4.jsonrpc.cache import LRUCache
13+
14+
15+
class CacheMetricsParams(BaseModel):
16+
model_config = ConfigDict(extra="forbid")
17+
18+
19+
class CacheMetricsResult(BaseModel):
20+
model_config = ConfigDict(extra="forbid")
21+
22+
size: int
23+
capacity: int
24+
hits: int
25+
misses: int
26+
evictions: int
27+
hit_rate: float
28+
29+
30+
def cache_metrics(
31+
params: CacheMetricsParams,
32+
*, cache: LRUCache | None = None,
33+
) -> CacheMetricsResult:
34+
if cache is None:
35+
return CacheMetricsResult(
36+
size=0, capacity=0, hits=0, misses=0,
37+
evictions=0, hit_rate=0.0,
38+
)
39+
s = cache.stats()
40+
return CacheMetricsResult(
41+
size=int(s["size"]),
42+
capacity=int(s["capacity"]),
43+
hits=int(s["hits"]),
44+
misses=int(s["misses"]),
45+
evictions=int(s["evictions"]),
46+
hit_rate=float(s["hit_rate"]),
47+
)
48+
49+
50+
__all__ = ["CacheMetricsParams", "CacheMetricsResult", "cache_metrics"]

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
LossSurfaceParams,
6868
loss_surface_run,
6969
)
70+
from cppmega_v4.jsonrpc.cache_metrics_method import (
71+
CacheMetricsParams,
72+
cache_metrics,
73+
)
7074
from cppmega_v4.jsonrpc.tokenizer_roundtrip_text_method import (
7175
TokenizerRoundtripTextParams,
7276
roundtrip_text,
@@ -196,6 +200,10 @@
196200
LossSurfaceParams,
197201
lambda p, c: loss_surface_run(p, cache=c),
198202
),
203+
"cache.metrics": (
204+
CacheMetricsParams,
205+
lambda p, c: cache_metrics(p, cache=c),
206+
),
199207
"tokenizer.roundtrip_text": (
200208
TokenizerRoundtripTextParams,
201209
lambda p, c: roundtrip_text(p, cache=c),

tests/v4/test_cache_metrics_rpc.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""V7-I07: cache.metrics JSON-RPC returns LRUCache stats snapshot."""
2+
3+
from __future__ import annotations
4+
5+
from cppmega_v4.jsonrpc.cache import LRUCache
6+
from cppmega_v4.jsonrpc.cache_metrics_method import (
7+
CacheMetricsParams, cache_metrics,
8+
)
9+
from cppmega_v4.jsonrpc.dispatcher import dispatch
10+
11+
12+
def test_v7_i07_cache_metrics_returns_zeros_for_empty():
13+
cache = LRUCache(capacity=8)
14+
r = cache_metrics(CacheMetricsParams(), cache=cache)
15+
assert r.size == 0
16+
assert r.capacity == 8
17+
assert r.hits == 0
18+
assert r.misses == 0
19+
assert r.evictions == 0
20+
assert r.hit_rate == 0.0
21+
22+
23+
def test_v7_i07_cache_metrics_counts_hits_misses():
24+
cache = LRUCache(capacity=4)
25+
cache.set("a", {"v": 1})
26+
cache.get("a") # hit
27+
cache.get("a") # hit
28+
cache.get("missing") # miss
29+
r = cache_metrics(CacheMetricsParams(), cache=cache)
30+
assert r.size == 1
31+
assert r.hits == 2
32+
assert r.misses == 1
33+
assert abs(r.hit_rate - (2 / 3)) < 1e-9
34+
35+
36+
def test_v7_i07_cache_metrics_counts_evictions():
37+
cache = LRUCache(capacity=2)
38+
cache.set("a", 1)
39+
cache.set("b", 2)
40+
cache.set("c", 3) # evicts a
41+
cache.set("d", 4) # evicts b
42+
r = cache_metrics(CacheMetricsParams(), cache=cache)
43+
assert r.size == 2
44+
assert r.evictions == 2
45+
46+
47+
def test_v7_i07_cache_metrics_handles_missing_cache():
48+
r = cache_metrics(CacheMetricsParams(), cache=None)
49+
assert r.size == 0
50+
assert r.hits == 0
51+
assert r.misses == 0
52+
assert r.hit_rate == 0.0
53+
54+
55+
def test_v7_i07_dispatcher_routes_cache_metrics():
56+
cache = LRUCache(capacity=4)
57+
cache.set("a", 1)
58+
cache.get("a")
59+
resp = dispatch({
60+
"jsonrpc": "2.0", "id": "T1", "method": "cache.metrics",
61+
"params": {},
62+
}, cache=cache)
63+
assert resp.error is None, resp.error
64+
assert resp.result is not None
65+
assert resp.result["hits"] == 1
66+
assert resp.result["size"] == 1
67+
assert resp.result["capacity"] == 4

0 commit comments

Comments
 (0)