Skip to content

Commit 90e4bd3

Browse files
committed
perf: fast startup staleness check, skip filesystem walk (v0.13.8)
On iree (60k files), is_stale() spent 10 seconds doing 8 rglob calls just to count files on every startup and tool call. This blocked the MCP server from becoming ready. Split is_stale() into fast (default) and full modes: - Fast: samples mtime of 100 cached file paths — no filesystem walk - Full: does the expensive rglob file count to detect new/deleted files Startup and tool handlers use fast mode (<0.1s). Periodic background check uses full mode (every 60s, non-blocking). Co-developed-by: Claude Code v2.1.39 (claude-opus-4-6)
1 parent bbc1425 commit 90e4bd3

3 files changed

Lines changed: 47 additions & 24 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"name": "context-daddy",
1010
"source": "./",
1111
"description": "Your codebase's context needs a responsible adult. Fast code exploration, living project narratives, and tribal knowledge that survives across sessions.",
12-
"version": "0.13.7",
12+
"version": "0.13.8",
1313
"author": {
1414
"name": "Robert Taylor"
1515
},

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "context-daddy",
3-
"version": "0.13.7",
3+
"version": "0.13.8",
44
"description": "Your codebase's context needs a responsible adult. Fast code exploration, living project narratives, and tribal knowledge that survives across sessions.",
55
"author": {
66
"name": "Robert Taylor"

servers/repo-map-server.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,17 @@ def check_indexing_watchdog():
258258
logger.error(f"Watchdog check failed: {e}")
259259

260260

261-
def is_stale() -> tuple[bool, str]:
261+
def is_stale(full_check: bool = False) -> tuple[bool, str]:
262262
"""
263263
Check if the repo map needs reindexing.
264264
Returns (is_stale, reason).
265+
266+
When full_check=False (default), uses cached file paths for mtime sampling
267+
and skips the expensive filesystem walk. This makes startup fast on large
268+
repos (iree: 10s → <0.1s).
269+
270+
When full_check=True, does a full file count to detect new/deleted files.
271+
Used by periodic_staleness_check (every 60s in background).
265272
"""
266273
indexer = get_indexer()
267274
db_path = get_db_path()
@@ -284,25 +291,37 @@ def is_stale() -> tuple[bool, str]:
284291
except (json.JSONDecodeError, IOError):
285292
return True, "cache file corrupt"
286293

287-
# Count files: prefer found_file_count (total files found on disk during last
288-
# index) over len(files) which only counts successfully cached files. This
289-
# avoids false positives when some files can't be read (broken symlinks, etc.)
290-
cached_count = cache_data.get("found_file_count", len(cache_data.get("files", {})))
291-
292-
# Quick file count check
293-
current_files = []
294-
for ext in [".py", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h", ".hxx"]:
295-
current_files.extend(indexer.find_files(project_root, {ext}))
296-
current_count = len(current_files)
297-
298-
if current_count != cached_count:
299-
return True, f"file count changed ({cached_count} cached, {current_count} found)"
300-
301-
# Check if any file is newer than DB
302-
db_mtime = db_path.stat().st_mtime
303-
for f in current_files[:100]: # Sample check for speed
304-
if f.stat().st_mtime > db_mtime:
305-
return True, "files modified since last index"
294+
if full_check:
295+
# Full filesystem walk — expensive on large repos but detects new/deleted files
296+
cached_count = cache_data.get("found_file_count", len(cache_data.get("files", {})))
297+
current_files = []
298+
for ext in [".py", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h", ".hxx"]:
299+
current_files.extend(indexer.find_files(project_root, {ext}))
300+
current_count = len(current_files)
301+
302+
if current_count != cached_count:
303+
return True, f"file count changed ({cached_count} cached, {current_count} found)"
304+
305+
# Sample mtime check from the full file list
306+
db_mtime = db_path.stat().st_mtime
307+
for f in current_files[:100]:
308+
if f.stat().st_mtime > db_mtime:
309+
return True, "files modified since last index"
310+
else:
311+
# Fast path: sample mtime check using cached file paths (no filesystem walk)
312+
db_mtime = db_path.stat().st_mtime
313+
cached_files = cache_data.get("files", {})
314+
sample_count = 0
315+
for rel_path in cached_files:
316+
full_path = project_root / rel_path
317+
try:
318+
if full_path.stat().st_mtime > db_mtime:
319+
return True, "files modified since last index"
320+
except OSError:
321+
continue # File may have been deleted — full_check will catch it
322+
sample_count += 1
323+
if sample_count >= 100:
324+
break
306325

307326
return False, "up to date"
308327

@@ -1325,13 +1344,17 @@ def md_list_figures(file_path: str) -> str:
13251344

13261345

13271346
async def periodic_staleness_check():
1328-
"""Periodically check if reindexing is needed."""
1347+
"""Periodically check if reindexing is needed.
1348+
1349+
Uses full_check=True to detect new/deleted files via filesystem walk.
1350+
This is expensive on large repos but runs in the background every 60s.
1351+
"""
13291352
while True:
13301353
await asyncio.sleep(STALENESS_CHECK_INTERVAL)
13311354
try:
13321355
is_indexing = _indexing_process is not None and _indexing_process.poll() is None
13331356
if not is_indexing:
1334-
stale, reason = is_stale()
1357+
stale, reason = is_stale(full_check=True)
13351358
if stale:
13361359
logger.info(f"Index is stale ({reason}), starting background reindex")
13371360
index_in_background()

0 commit comments

Comments
 (0)