perf: graph cache with write-invalidation in build_graph()

build_graph() scans every drawer's metadata in 1000-item batches on
every call — O(n) per graph build with no caching. At 50K+ drawers
this costs several seconds per MCP tool call (traverse, find_tunnels,
graph_stats all call build_graph on every invocation).

Add a module-level cache (nodes + edges + timestamp) with a 60-second
TTL. Cache is invalidated via invalidate_graph_cache(), exported for
write operations to call. Tests updated with setup_method cache resets
and two new tests verifying cache hit and invalidation behaviour.
This commit is contained in:
jp
2026-04-11 19:44:49 -07:00
parent d4c942417a
commit 84e2aa16e4
2 changed files with 64 additions and 0 deletions
+27
View File
@@ -18,6 +18,7 @@ No external graph DB needed — built from ChromaDB metadata.
import hashlib
import json
import os
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
@@ -25,6 +26,20 @@ from .config import MempalaceConfig
from .palace import get_collection as _get_palace_collection
from .palace import mine_lock
# Module-level graph cache — mirrors _metadata_cache pattern in mcp_server.py
_graph_cache_nodes = None
_graph_cache_edges = None
_graph_cache_time = 0.0
_GRAPH_CACHE_TTL = 60.0 # seconds — graph changes less often than metadata
def invalidate_graph_cache():
"""Clear the graph cache. Called from mcp_server.py on writes."""
global _graph_cache_nodes, _graph_cache_edges, _graph_cache_time
_graph_cache_nodes = None
_graph_cache_edges = None
_graph_cache_time = 0.0
def _get_collection(config=None):
config = config or MempalaceConfig()
@@ -42,10 +57,18 @@ def build_graph(col=None, config=None):
"""
Build the palace graph from ChromaDB metadata.
Returns cached result if fresh (within TTL). Cache is invalidated
on writes via invalidate_graph_cache().
Returns:
nodes: dict of {room: {wings: set, halls: set, count: int}}
edges: list of {room, wing_a, wing_b, hall} — one per tunnel crossing
"""
global _graph_cache_nodes, _graph_cache_edges, _graph_cache_time
now = time.time()
if _graph_cache_nodes is not None and (now - _graph_cache_time) < _GRAPH_CACHE_TTL:
return _graph_cache_nodes, _graph_cache_edges
if col is None:
col = _get_collection(config)
if not col:
@@ -101,6 +124,10 @@ def build_graph(col=None, config=None):
"dates": sorted(data["dates"])[-5:] if data["dates"] else [],
}
_graph_cache_nodes = nodes
_graph_cache_edges = edges
_graph_cache_time = time.time()
return nodes, edges