22
33import importlib
44import io
5+ import json
56import os
67import re
8+ import time
79from pathlib import Path
810from types import SimpleNamespace
911
2628from tidy3d .web .api .autograd .constants import SIM_VJP_FILE
2729from tidy3d .web .api .container import Batch , WebContainer
2830from 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+ )
3040from tidy3d .web .core .task_core import BatchTask
3141
3242common .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):
223245def _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+
552691def _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