Skip to content

Commit e73fbb4

Browse files
marcorudolphflexyaugenst-flex
authored andcommitted
feat(tidy3d): FXC-3902-make-manipulations-on-local-cache-more-performant
1 parent 9ad91f1 commit e73fbb4

File tree

2 files changed

+525
-81
lines changed

2 files changed

+525
-81
lines changed

tests/test_web/test_local_cache.py

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import importlib
44
import io
5+
import json
56
import os
67
import re
8+
import time
79
from pathlib import Path
810
from types import SimpleNamespace
911

@@ -26,7 +28,15 @@
2628
from tidy3d.web.api.autograd.constants import SIM_VJP_FILE
2729
from tidy3d.web.api.container import Batch, WebContainer
2830
from tidy3d.web.api.webapi import load_simulation_if_cached
29-
from tidy3d.web.cache import CACHE_ARTIFACT_NAME, clear, get_cache_entry_dir, resolve_local_cache
31+
from tidy3d.web.cache import (
32+
CACHE_ARTIFACT_NAME,
33+
CACHE_STATS_NAME,
34+
TMP_BATCH_PREFIX,
35+
TMP_PREFIX,
36+
clear,
37+
get_cache_entry_dir,
38+
resolve_local_cache,
39+
)
3040
from tidy3d.web.core.task_core import BatchTask
3141

3242
common.CONNECTION_RETRY_TIME = 0.1
@@ -147,6 +157,17 @@ def _fake_download(*, task_id, path, **kwargs):
147157
if sim is not None:
148158
PATH_TO_SIM[str(Path(path))] = sim
149159

160+
def _fake_load_simulation(task_id, path="simulation.json", verbose=True):
161+
sim = TASK_TO_SIM.get(task_id)
162+
if sim is None:
163+
sim = next(iter(PATH_TO_SIM.values()), None)
164+
if sim is None:
165+
raise RuntimeError(f"No simulation mapped for task_id {task_id}")
166+
path_obj = Path(path)
167+
path_obj.parent.mkdir(parents=True, exist_ok=True)
168+
path_obj.write_text("{}")
169+
return sim
170+
150171
def _fake__check_folder(*args, **kwargs):
151172
pass
152173

@@ -170,6 +191,7 @@ def _fake_from_file(*args, **kwargs):
170191
monkeypatch.setattr(web, "start", _fake_start)
171192
monkeypatch.setattr(web, "monitor", _fake_monitor)
172193
monkeypatch.setattr(web, "download", _fake_download)
194+
monkeypatch.setattr(web, "load_simulation", _fake_load_simulation)
173195
monkeypatch.setattr(web, "estimate_cost", lambda *args, **kwargs: 0.0)
174196
monkeypatch.setattr(Job, "status", property(_fake_status))
175197
monkeypatch.setattr(engine, "upload_sim_fields_keys", lambda *args, **kwargs: None)
@@ -223,13 +245,16 @@ def _test_run_cache_hit(monkeypatch, tmp_path, basic_simulation, fake_data):
223245
def _test_load_simulation_if_cached(monkeypatch, tmp_path, basic_simulation):
224246
counters = _patch_run_pipeline(monkeypatch)
225247
out_path = tmp_path / "result_load_simulation_if_cached.hdf5"
226-
clear()
248+
cache = resolve_local_cache(True)
249+
cache.clear()
227250

228251
data = web.run(basic_simulation, task_name="demo", path=str(out_path))
229252
assert isinstance(data, _FakeStubData)
230253
assert counters == {"upload": 1, "start": 1, "monitor": 1, "download": 1}
254+
assert len(cache) == 1
231255

232256
sim_data_from_cache = load_simulation_if_cached(basic_simulation)
257+
assert sim_data_from_cache is not None
233258
assert sim_data_from_cache.simulation == basic_simulation
234259

235260
out_path2 = tmp_path / "result_load_simulation_if_cached2.hdf5"
@@ -549,6 +574,120 @@ def _test_cache_eviction_by_size(monkeypatch, tmp_path_factory, basic_simulation
549574
assert entries[0]["simulation_hash"] == sim2._hash_self()
550575

551576

577+
def _test_cache_stats_tracking(monkeypatch, tmp_path_factory, basic_simulation):
578+
monkeypatch.setattr(config.local_cache, "max_entries", 10)
579+
cache = resolve_local_cache(use_cache=True)
580+
cache.clear()
581+
582+
artifact = tmp_path_factory.mktemp("artifact_stats") / CACHE_ARTIFACT_NAME
583+
payload = "stats-payload"
584+
artifact.write_text(payload)
585+
586+
cache.store_result(_FakeStubData(basic_simulation), MOCK_TASK_ID, str(artifact), "FDTD")
587+
588+
stats_path = cache.root / CACHE_STATS_NAME
589+
assert stats_path.exists()
590+
stats = json.loads(stats_path.read_text())
591+
assert stats["total_entries"] == 1
592+
assert stats["total_size"] == len(payload)
593+
key = next(iter(stats["last_used"]))
594+
entry_last_used = stats["last_used"][key]
595+
assert isinstance(entry_last_used, str)
596+
597+
time.sleep(0.001)
598+
cache_entry = cache._fetch(key)
599+
assert cache_entry is not None
600+
601+
updated_stats = json.loads(stats_path.read_text())
602+
assert updated_stats["total_size"] == len(payload)
603+
assert updated_stats["last_used"][key] != entry_last_used
604+
605+
cache.invalidate(key)
606+
final_stats = json.loads(stats_path.read_text())
607+
assert final_stats["total_entries"] == 0
608+
assert final_stats["total_size"] == 0
609+
assert final_stats["last_used"] == {}
610+
611+
cache.clear()
612+
613+
614+
def _test_cache_stats_sync(monkeypatch, tmp_path_factory, basic_simulation):
615+
monkeypatch.setattr(config.local_cache, "max_entries", 10)
616+
cache = resolve_local_cache(use_cache=True)
617+
cache.clear()
618+
619+
sim1 = basic_simulation
620+
sim2 = basic_simulation.updated_copy(shutoff=2e-4)
621+
622+
artifact1 = tmp_path_factory.mktemp("artifact_sync1") / CACHE_ARTIFACT_NAME
623+
payload1 = "sync-one"
624+
artifact1.write_text(payload1)
625+
cache.store_result(_FakeStubData(sim1), f"{MOCK_TASK_ID}-1", str(artifact1), "FDTD")
626+
627+
artifact2 = tmp_path_factory.mktemp("artifact_sync2") / CACHE_ARTIFACT_NAME
628+
payload2 = "sync-two"
629+
artifact2.write_text(payload2)
630+
cache.store_result(_FakeStubData(sim2), f"{MOCK_TASK_ID}-2", str(artifact2), "FDTD")
631+
632+
stats_path = cache.root / CACHE_STATS_NAME
633+
assert stats_path.exists()
634+
635+
stats_path.unlink()
636+
assert not stats_path.exists()
637+
638+
rebuilt = cache.sync_stats()
639+
assert stats_path.exists()
640+
assert rebuilt.total_entries == 2
641+
assert rebuilt.total_size == len(payload1) + len(payload2)
642+
643+
keys = {meta["cache_key"] for meta in cache.list()}
644+
assert set(rebuilt.last_used.keys()) == keys
645+
for info in rebuilt.last_used.values():
646+
assert isinstance(info, str)
647+
648+
cache.clear()
649+
650+
651+
def _test_store_and_fetch_do_not_iterate(monkeypatch, tmp_path, basic_simulation):
652+
cache = resolve_local_cache(use_cache=True)
653+
cache.clear()
654+
655+
original_iter = cache._iter_entries
656+
iter_calls = {"count": 0}
657+
658+
def _counting_iter():
659+
iter_calls["count"] += 1
660+
yield from original_iter()
661+
662+
monkeypatch.setattr(cache, "_iter_entries", _counting_iter)
663+
664+
artifact = tmp_path / "iter_guard.hdf5"
665+
artifact.write_text("payload")
666+
667+
cache.store_result(_FakeStubData(basic_simulation), MOCK_TASK_ID, str(artifact), "FDTD")
668+
assert iter_calls["count"] == 0
669+
670+
entry_dirs = []
671+
for prefix_dir in cache.root.iterdir():
672+
if not prefix_dir.is_dir() or prefix_dir.name.startswith((TMP_PREFIX, TMP_BATCH_PREFIX)):
673+
continue
674+
# collect child entry dirs (support nested layout)
675+
for child in prefix_dir.iterdir():
676+
if not child.is_dir() or child.name.startswith((TMP_PREFIX, TMP_BATCH_PREFIX)):
677+
continue
678+
entry_dirs.append(child.name)
679+
680+
assert entry_dirs, "Expected stored cache entry"
681+
key = entry_dirs[0]
682+
683+
before_fetch = iter_calls["count"]
684+
fetched_entry = cache._fetch(key)
685+
assert fetched_entry is not None
686+
assert iter_calls["count"] == before_fetch
687+
688+
cache.clear()
689+
690+
552691
def _test_configure_cache_roundtrip(monkeypatch, tmp_path):
553692
monkeypatch.setattr(config.local_cache, "enabled", True)
554693
monkeypatch.setattr(config.local_cache, "directory", tmp_path)
@@ -591,18 +730,19 @@ def test_cache_sequential(
591730
"""Run all critical cache tests in sequence to ensure stability."""
592731
monkeypatch.setattr(config.local_cache, "enabled", True)
593732

594-
# this at first as runtime changes overrides env
595733
_test_env_var_overrides(monkeypatch, tmp_path)
596-
597734
_test_load_simulation_if_cached(monkeypatch, tmp_path, basic_simulation)
598735
_test_run_cache_hit(monkeypatch, tmp_path, basic_simulation, fake_data)
599736
_test_load_cache_hit(monkeypatch, tmp_path, basic_simulation, fake_data)
600737
_test_checksum_mismatch_triggers_refresh(monkeypatch, tmp_path, basic_simulation)
601738
_test_cache_eviction_by_entries(monkeypatch, tmp_path_factory, basic_simulation)
602739
_test_cache_eviction_by_size(monkeypatch, tmp_path_factory, basic_simulation)
740+
_test_cache_stats_tracking(monkeypatch, tmp_path_factory, basic_simulation)
741+
_test_cache_stats_sync(monkeypatch, tmp_path_factory, basic_simulation)
603742
_test_run_cache_hit_async(monkeypatch, basic_simulation, tmp_path)
604743
_test_job_run_cache(monkeypatch, basic_simulation, tmp_path)
605744
_test_autograd_cache(monkeypatch, request)
606745
_test_configure_cache_roundtrip(monkeypatch, tmp_path)
746+
_test_store_and_fetch_do_not_iterate(monkeypatch, tmp_path, basic_simulation)
607747
_test_mode_solver_caching(monkeypatch, tmp_path)
608748
_test_verbosity(monkeypatch, basic_simulation)

0 commit comments

Comments
 (0)