_read_profile_model_config() fires on every chat-open that hits the
resolve_model=1 path (one of the 3 calls in the chat-open waterfall).
For sessions under a named profile (non-default), the function
performs:
- 1× os.path.isfile() call
- 1× open() of config.yaml
- 1× yaml.safe_load() of the full file
On a Celeron N3350 with eMMC the open+safe_load alone is single-digit
ms per call, repeated on every chat-open. The /api/session 24h log
showed ?messages=0&resolve_model=1 at p50 1514ms with this YAML
parse as one of the dominant costs.
This wraps the parse in a per-process cache:
- key = (profile_name, mtime, size)
- value = parsed dict + monotonic timestamp
- TTL 60s as a backstop for filesystem mtime resolution issues
- cap 32 entries (bounded; profiles are bounded in practice)
mtime+size auto-invalidates on edit. The TTL only fires when mtime
resolution is too coarse to detect a same-second write; in normal
operation, every config edit lands as a different mtime and the next
chat-open rebuilds the cache entry.
Threading: a single Lock guards the dict. yaml.safe_load is the slow
step, not the lock, so contention is bounded. The lock is held for
microseconds on hit and milliseconds on miss (parse).
Field-semantics check: the function's signature is unchanged
((provider, default, config_dict) tuple) and the only call sites
(_resolve_effective_session_model_for_display and its sibling)
treat it the same as before. The cache stores the parsed dict;
the function still does the per-call (provider, default) extraction
inline so the tuple's content is identical to the pre-patch path.
No public API change. No behavior change observable other than
faster chat-open for sessions under named profiles.