Skip to content

Commit 3fec6f1

Browse files
sbryngelsonclaude
andcommitted
Address review: bounded caches, MP4 cleanup, avoid double-read
- interactive.py/tui.py: cap _cache at 50 entries (FIFO eviction) to prevent OOM in long sessions loading many large timesteps - renderer.py: wrap frame-rendering loop in try/finally so temp dir is always cleaned up even if a render_* call raises (e.g. ValueError from render_3d_slice) - viz.py: reuse test_assembled for first step instead of reading it twice Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 911b10a commit 3fec6f1

4 files changed

Lines changed: 62 additions & 43 deletions

File tree

toolchain/mfc/viz/interactive.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,20 @@
4949
_YELLOW = '#f9e2af'
5050

5151
# ---------------------------------------------------------------------------
52-
# Server-side data cache {step -> AssembledData}
52+
# Server-side data cache {step -> AssembledData} (bounded to avoid OOM)
5353
# ---------------------------------------------------------------------------
54+
_CACHE_MAX = 50
5455
_cache: dict = {}
56+
_cache_order: list = []
5557

5658

5759
def _load(step: int, read_func: Callable):
5860
if step not in _cache:
61+
if len(_cache) >= _CACHE_MAX:
62+
evict = _cache_order.pop(0)
63+
_cache.pop(evict, None)
5964
_cache[step] = read_func(step)
65+
_cache_order.append(step)
6066
return _cache[step]
6167

6268

toolchain/mfc/viz/renderer.py

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -338,42 +338,57 @@ def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-argum
338338
os.makedirs(output_dir, exist_ok=True)
339339
viz_dir = tempfile.mkdtemp(dir=output_dir, prefix='_frames_')
340340

341+
def _cleanup():
342+
for fname in sorted(f for f in os.listdir(viz_dir) if f.endswith('.png')):
343+
try:
344+
os.remove(os.path.join(viz_dir, fname))
345+
except OSError:
346+
pass
347+
try:
348+
os.rmdir(viz_dir)
349+
except OSError:
350+
pass
351+
341352
try:
342353
from tqdm import tqdm # pylint: disable=import-outside-toplevel
343354
step_iter = tqdm(steps, desc='Rendering frames')
344355
except ImportError:
345356
step_iter = steps
346357

347-
for i, step in enumerate(step_iter):
348-
assembled = read_func(step)
349-
frame_path = os.path.join(viz_dir, f'{i:06d}.png')
350-
351-
if tiled and assembled.ndim == 1:
352-
render_1d_tiled(assembled.x_cc, assembled.variables,
353-
step, frame_path, **opts)
354-
elif assembled.ndim == 1:
355-
var_data = assembled.variables.get(varname)
356-
if var_data is None:
357-
continue
358-
render_1d(assembled.x_cc, var_data,
359-
varname, step, frame_path, **opts)
360-
elif assembled.ndim == 2:
361-
var_data = assembled.variables.get(varname)
362-
if var_data is None:
363-
continue
364-
render_2d(assembled.x_cc, assembled.y_cc,
365-
var_data,
366-
varname, step, frame_path, **opts)
367-
elif assembled.ndim == 3:
368-
var_data = assembled.variables.get(varname)
369-
if var_data is None:
370-
continue
371-
render_3d_slice(assembled, varname, step, frame_path, **opts)
372-
else:
373-
raise ValueError(
374-
f"Unsupported dimensionality ndim={assembled.ndim} for step {step}. "
375-
"Expected 1, 2, or 3."
376-
)
358+
try:
359+
for i, step in enumerate(step_iter):
360+
assembled = read_func(step)
361+
frame_path = os.path.join(viz_dir, f'{i:06d}.png')
362+
363+
if tiled and assembled.ndim == 1:
364+
render_1d_tiled(assembled.x_cc, assembled.variables,
365+
step, frame_path, **opts)
366+
elif assembled.ndim == 1:
367+
var_data = assembled.variables.get(varname)
368+
if var_data is None:
369+
continue
370+
render_1d(assembled.x_cc, var_data,
371+
varname, step, frame_path, **opts)
372+
elif assembled.ndim == 2:
373+
var_data = assembled.variables.get(varname)
374+
if var_data is None:
375+
continue
376+
render_2d(assembled.x_cc, assembled.y_cc,
377+
var_data,
378+
varname, step, frame_path, **opts)
379+
elif assembled.ndim == 3:
380+
var_data = assembled.variables.get(varname)
381+
if var_data is None:
382+
continue
383+
render_3d_slice(assembled, varname, step, frame_path, **opts)
384+
else:
385+
raise ValueError(
386+
f"Unsupported dimensionality ndim={assembled.ndim} for step {step}. "
387+
"Expected 1, 2, or 3."
388+
)
389+
except Exception:
390+
_cleanup()
391+
raise
377392

378393
# Combine frames into MP4 using imageio + imageio-ffmpeg (bundled ffmpeg)
379394
frame_files = sorted(f for f in os.listdir(viz_dir) if f.endswith('.png'))
@@ -390,14 +405,5 @@ def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-argum
390405
except (OSError, ValueError, RuntimeError) as exc:
391406
print(f"imageio MP4 write failed: {exc}")
392407
finally:
393-
# Always clean up temporary frame files
394-
for fname in frame_files:
395-
try:
396-
os.remove(os.path.join(viz_dir, fname))
397-
except OSError:
398-
pass
399-
try:
400-
os.rmdir(viz_dir)
401-
except OSError:
402-
pass
408+
_cleanup()
403409
return success

toolchain/mfc/viz/tui.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,20 @@
3838
]
3939

4040
# ---------------------------------------------------------------------------
41-
# Step cache {step -> AssembledData}
41+
# Step cache {step -> AssembledData} (bounded to avoid OOM)
4242
# ---------------------------------------------------------------------------
43+
_CACHE_MAX = 50
4344
_cache: Dict[int, object] = {}
45+
_cache_order: List[int] = []
4446

4547

4648
def _load(step: int, read_func: Callable) -> object:
4749
if step not in _cache:
50+
if len(_cache) >= _CACHE_MAX:
51+
evict = _cache_order.pop(0)
52+
_cache.pop(evict, None)
4853
_cache[step] = read_func(step)
54+
_cache_order.append(step)
4955
return _cache[step]
5056

5157

toolchain/mfc/viz/viz.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def read_step(step):
302302
label = 'all' if tiled else varname
303303
for step in step_iter:
304304
try:
305-
assembled = read_step(step)
305+
# Reuse the already-loaded probe data for the first step
306+
assembled = test_assembled if step == requested_steps[0] else read_step(step)
306307
except (FileNotFoundError, EOFError, ValueError) as exc:
307308
cons.print(f"[yellow]Warning:[/yellow] Skipping step {step}: {exc}")
308309
failures.append(step)

0 commit comments

Comments
 (0)