Skip to content

Commit 1d12175

Browse files
sbryngelsonclaude
andcommitted
Address Claude Code review: bounded caches, MP4 cleanup, avoid double-read
#2 (weak sentinel): _ensure_viz_deps() now checks all five key packages (matplotlib, imageio, h5py, textual, dash) so a user with matplotlib pre-installed (Anaconda, etc.) but missing the other viz deps still triggers the install rather than getting raw ImportErrors later. #5 (duplicate cache): Extract bounded FIFO cache to _step_cache.py with load(), seed(), and CACHE_MAX. tui.py and interactive.py now import from the shared module. test_viz.py TestTuiCache tests _step_cache directly and gains a test_seed_clears_and_populates case. Also removes unused Dict import from tui.py (no longer needed after the _cache type annotation moved to _step_cache.py). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a84b3f3 commit 1d12175

5 files changed

Lines changed: 64 additions & 50 deletions

File tree

toolchain/mfc/viz/_step_cache.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Bounded FIFO step cache shared by tui.py and interactive.py.
2+
3+
Keeps up to CACHE_MAX assembled timesteps in memory, evicting the oldest
4+
entry when the cap is reached. The module-level state is intentional:
5+
both the TUI and the interactive server are single-instance; a per-session
6+
cache avoids redundant disk reads while bounding peak memory usage.
7+
"""
8+
9+
from typing import Callable
10+
11+
CACHE_MAX: int = 50
12+
_cache: dict = {}
13+
_cache_order: list = []
14+
15+
16+
def load(step: int, read_func: Callable) -> object:
17+
"""Return cached data for *step*, calling *read_func* on a miss."""
18+
if step not in _cache:
19+
if len(_cache) >= CACHE_MAX:
20+
evict = _cache_order.pop(0)
21+
_cache.pop(evict, None)
22+
_cache[step] = read_func(step)
23+
_cache_order.append(step)
24+
return _cache[step]
25+
26+
27+
def seed(step: int, data: object) -> None:
28+
"""Clear the cache and pre-populate it with already-loaded data."""
29+
_cache.clear()
30+
_cache_order.clear()
31+
_cache[step] = data
32+
_cache_order.append(step)

toolchain/mfc/viz/interactive.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from dash import Dash, dcc, html, Input, Output, State, callback_context, no_update
1616

1717
from mfc.printer import cons
18+
from . import _step_cache
1819

1920
# ---------------------------------------------------------------------------
2021
# Colormaps available in the picker
@@ -51,19 +52,8 @@
5152
# ---------------------------------------------------------------------------
5253
# Server-side data cache {step -> AssembledData} (bounded to avoid OOM)
5354
# ---------------------------------------------------------------------------
54-
_CACHE_MAX = 50
55-
_cache: dict = {}
56-
_cache_order: list = []
57-
58-
59-
def _load(step: int, read_func: Callable):
60-
if step not in _cache:
61-
if len(_cache) >= _CACHE_MAX:
62-
evict = _cache_order.pop(0)
63-
_cache.pop(evict, None)
64-
_cache[step] = read_func(step)
65-
_cache_order.append(step)
66-
return _cache[step]
55+
_load = _step_cache.load
56+
_CACHE_MAX = _step_cache.CACHE_MAX
6757

6858

6959
# ---------------------------------------------------------------------------

toolchain/mfc/viz/test_viz.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -453,13 +453,13 @@ def test_typo_suggests_correct(self):
453453
# ---------------------------------------------------------------------------
454454

455455
class TestTuiCache(unittest.TestCase):
456-
"""Test that the TUI step cache respects CACHE_MAX."""
456+
"""Test that the shared step cache respects CACHE_MAX."""
457457

458458
def setUp(self):
459-
import mfc.viz.tui as tui_mod
460-
self._mod = tui_mod
461-
tui_mod._cache.clear()
462-
tui_mod._cache_order.clear()
459+
import mfc.viz._step_cache as cache_mod
460+
self._mod = cache_mod
461+
cache_mod._cache.clear()
462+
cache_mod._cache_order.clear()
463463

464464
def tearDown(self):
465465
self._mod._cache.clear()
@@ -470,7 +470,7 @@ def _read(self, step):
470470

471471
def test_cache_stores_entry(self):
472472
"""Loaded step is stored in cache."""
473-
self._mod._load(0, self._read)
473+
self._mod.load(0, self._read)
474474
self.assertIn(0, self._mod._cache)
475475

476476
def test_cache_hit_avoids_reload(self):
@@ -479,19 +479,26 @@ def test_cache_hit_avoids_reload(self):
479479
def counting(step):
480480
calls[0] += 1
481481
return step
482-
self._mod._load(5, counting)
483-
self._mod._load(5, counting)
482+
self._mod.load(5, counting)
483+
self._mod.load(5, counting)
484484
self.assertEqual(calls[0], 1)
485485

486486
def test_cache_evicts_oldest_at_cap(self):
487487
"""Oldest entry is evicted when CACHE_MAX is exceeded."""
488-
cap = self._mod._CACHE_MAX
488+
cap = self._mod.CACHE_MAX
489489
for i in range(cap + 3):
490-
self._mod._load(i, self._read)
490+
self._mod.load(i, self._read)
491491
self.assertLessEqual(len(self._mod._cache), cap)
492492
self.assertNotIn(0, self._mod._cache) # first evicted
493493
self.assertIn(cap + 2, self._mod._cache) # most recent kept
494494

495+
def test_seed_clears_and_populates(self):
496+
"""seed() clears existing cache and pre-loads one entry."""
497+
self._mod.load(99, self._read) # put something in first
498+
self._mod.seed(0, "preloaded")
499+
self.assertEqual(len(self._mod._cache), 1)
500+
self.assertEqual(self._mod._cache[0], "preloaded")
501+
495502

496503
# ---------------------------------------------------------------------------
497504
# Tests: log scale rendering (new feature smoke tests)

toolchain/mfc/viz/tui.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"""
1212
from __future__ import annotations
1313

14-
from typing import Callable, Dict, List, Optional
14+
from typing import Callable, List, Optional
1515

1616
import numpy as np
1717

@@ -30,29 +30,16 @@
3030
from textual_plotext import PlotextPlot
3131

3232
from mfc.printer import cons
33+
from . import _step_cache
3334

3435
# Colormaps available via [c] cycling
3536
_CMAPS: List[str] = [
3637
'viridis', 'plasma', 'inferno', 'magma', 'cividis',
3738
'coolwarm', 'RdBu_r', 'seismic', 'gray',
3839
]
3940

40-
# ---------------------------------------------------------------------------
41-
# Step cache {step -> AssembledData} (bounded to avoid OOM)
42-
# ---------------------------------------------------------------------------
43-
_CACHE_MAX = 50
44-
_cache: Dict[int, object] = {}
45-
_cache_order: List[int] = []
46-
47-
48-
def _load(step: int, read_func: Callable) -> object:
49-
if step not in _cache:
50-
if len(_cache) >= _CACHE_MAX:
51-
evict = _cache_order.pop(0)
52-
_cache.pop(evict, None)
53-
_cache[step] = read_func(step)
54-
_cache_order.append(step)
55-
return _cache[step]
41+
_load = _step_cache.load
42+
_CACHE_MAX = _step_cache.CACHE_MAX
5643

5744

5845
# ---------------------------------------------------------------------------
@@ -456,8 +443,7 @@ def run_tui(
456443
)
457444
cons.print("[dim] ,/. or ←/→ prev/next step • space play • l log • f freeze • ↑↓ variable • q quit[/dim]")
458445

459-
_cache.clear()
460-
_cache[steps[0]] = first
446+
_step_cache.seed(steps[0], first)
461447

462448
app = MFCTuiApp(
463449
steps=steps,

toolchain/mfc/viz/viz.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import os
88
import importlib
9+
import importlib.util
910
import shutil
1011
import subprocess
1112
import sys
@@ -18,15 +19,13 @@
1819
def _ensure_viz_deps() -> None:
1920
"""Install the [viz] optional extras on first use.
2021
21-
Checks for matplotlib as the sentinel package. If it is missing the
22-
whole viz extra is assumed to be absent and gets installed via uv (or pip
23-
as a fallback) from the local toolchain directory.
22+
Checks one sentinel per feature group so that a user who has matplotlib
23+
pre-installed (e.g. via Anaconda) but lacks imageio, textual, or h5py
24+
still triggers the install.
2425
"""
25-
try:
26-
import matplotlib # noqa: F401 # pylint: disable=import-outside-toplevel,unused-import
27-
return # already installed
28-
except ImportError:
29-
pass
26+
_SENTINELS = ("matplotlib", "imageio", "h5py", "textual", "dash")
27+
if all(importlib.util.find_spec(p) is not None for p in _SENTINELS):
28+
return # all present
3029

3130
toolchain_path = os.path.join(MFC_ROOT_DIR, "toolchain")
3231
cons.print("[bold]Installing viz dependencies[/bold] "

0 commit comments

Comments
 (0)