From f8cd40d74a28eeac91a5e3989046800c510564c4 Mon Sep 17 00:00:00 2001 From: Konstantin M Date: Thu, 9 Jul 2026 00:28:26 +0000 Subject: [PATCH] perf(webui/session-load-latency) tier3a: Greptile follow-up fixes (P1 empty-scenes skip, P2 draft-log sink, P2 config-cache inode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three unaddressed Greptile review comments fixed: P1 - Empty scenes skip check (api/models.py): When the cached session had no anchor_activity_scenes, the scene-comparison branch was skipped entirely by the 'if cached_scenes:' guard. An inactive session would return False (cache fresh) even when disk had gained its first scene record. Add an else clause that checks disk for scene records when the cache has none. P2 - Draft slow log sink (api/routes.py): The /api/session/draft handler's slow-path log (>200ms) used logger.warning, which is silently dropped in the WebUI process (root logger has no handler). Route through handler._safe_webui_print() instead, the same fix applied to the /api/session slow log in tier2c. P2 - Config cache same-timestamp stale (api/routes.py): The profile config.yaml cache key (profile_name, mtime, size) could collide on coarse-mtime filesystems (FAT, some network FS) when a config edit within the same timestamp tick happened to produce the same byte count. Add st_ino (inode) to the cache key — most editors use atomic-rename writes which assign a new inode, so the key changes even when mtime+size are identical. --- api/models.py | 9 +++++++++ api/routes.py | 21 ++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/api/models.py b/api/models.py index f6720390..702fc699 100644 --- a/api/models.py +++ b/api/models.py @@ -3152,6 +3152,15 @@ def _cached_session_lags_disk(cached) -> bool: return latest if _max_updated(disk_scenes) > _max_updated(cached_scenes): return True + else: + # Cached session has no scene records. Check if disk has gained + # the first scene record — without this the fast-path would miss + # a newly persisted scene and return the stale cache. Greptile P1. + disk_meta_quick = _persisted_session_meta_prefix(sid) + if disk_meta_quick is not None: + disk_scenes = disk_meta_quick.get('anchor_activity_scenes') or {} + if isinstance(disk_scenes, dict) and disk_scenes: + return True if getattr(cached, 'active_stream_id', None) or getattr(cached, 'pending_user_message', None): # Active session: messages may be in flight; fall through to the # full check to be safe. diff --git a/api/routes.py b/api/routes.py index 75474a5a..d04bdc92 100644 --- a/api/routes.py +++ b/api/routes.py @@ -6309,10 +6309,11 @@ def _read_profile_model_config( # perf(webui/session-load-latency) tier2a: process-wide cache for parsed -# profile config.yaml. Key = (profile_name, mtime, size); value = parsed -# dict. mtime+size auto-invalidates on edit; a 60s TTL is the backstop -# in case of coarse mtime resolution (some network filesystems round -# mtime to whole seconds — the size guard catches a write of equal-length +# profile config.yaml. Key = (profile_name, inode, mtime, size); value = parsed +# dict. inode tracks atomic-rename edits (most editors replace files, giving a +# new inode on Linux). mtime+size auto-invalidates on in-place edits; a 60s TTL +# is the backstop in case of coarse mtime resolution (some network filesystems +# round mtime to whole seconds — the size guard catches a write of equal-length # bytes within the same second). Reads are guarded by a single Lock to # keep the hot path simple; the underlying yaml.safe_load is the slow # step, not the lock, so contention is bounded. @@ -6329,7 +6330,8 @@ def _read_profile_config_cached(profile_name: str, cfg_path: str) -> dict | None return None mtime = float(getattr(st, "st_mtime", 0.0) or 0.0) size = int(getattr(st, "st_size", 0) or 0) - key = (str(profile_name or ""), mtime, size) + inode = int(getattr(st, "st_ino", 0) or 0) + key = (str(profile_name or ""), inode, mtime, size) now = time.monotonic() with _PROFILE_CONFIG_CACHE_LOCK: cached = _PROFILE_CONFIG_CACHE.get(key) @@ -13807,10 +13809,11 @@ def handle_post(handler, parsed) -> bool: f"{n}={((t - prev[1]) * 1000):.1f}ms" for (n, t), prev in zip(_draft_stages[1:], _draft_stages[:-1]) ) - logger.warning( - "[SLOW] /api/session/draft total=%.1fms stages: %s", - (_draft_stages[-1][1] - _draft_t0) * 1000, - parts, + handler._safe_webui_print( + "[SLOW] /api/session/draft total=%.1fms stages: %s" % ( + (_draft_stages[-1][1] - _draft_t0) * 1000, + parts, + ) ) return True