Skip to content

Commit c2dc17f

Browse files
perf(viewer): share one mtime-keyed cache for the trends CSV and dir listing
trends_cache.csv was reparsed on every render by the Status tab, the Charts tab and the run detail view, and the results directory was relisted three times per page load. On the GCS FUSE mount each listing costs one stat per run directory, so both scaled with the number of runs and ran on interactions that needed neither. Add load_trends_df and list_run_dirs, keyed on mtime, and route every caller through them. load_summaries now builds on load_trends_df so the CSV is parsed once rather than twice. The frame is now shared, so the Charts filter chain uses assign() rather than an in-place column write, which would otherwise have welded product_dataset onto the cached object.
1 parent 8d106d5 commit c2dc17f

2 files changed

Lines changed: 89 additions & 63 deletions

File tree

viewer/main.py

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,70 @@ def get_results_dir():
133133
return results_dir_candidates[1] # Fallback to default
134134

135135

136+
# (mtime, value). Each is replaced as a whole so a concurrent reader never sees a
137+
# value that disagrees with the mtime it was built from.
138+
_TRENDS_DF_CACHE = (None, None)
139+
_RUN_DIRS_CACHE = (None, [])
140+
141+
142+
def _mtime(path):
143+
try:
144+
return os.path.getmtime(path)
145+
except OSError:
146+
return None
147+
148+
149+
def load_trends_df(results_dir):
150+
"""Parsed trends_cache.csv, reread only when the file changes.
151+
152+
Shared by every request in this worker, so callers must not mutate it.
153+
"""
154+
global _TRENDS_DF_CACHE
155+
156+
cache_file = os.path.join(results_dir, "trends_cache.csv")
157+
mtime = _mtime(cache_file)
158+
if mtime is None:
159+
logging.warning(f"Trends cache file not found at {cache_file}")
160+
return None
161+
162+
cached_mtime, df = _TRENDS_DF_CACHE
163+
if cached_mtime == mtime:
164+
return df
165+
166+
try:
167+
df = pd.read_csv(cache_file)
168+
except Exception as e:
169+
logging.error(f"Error reading trends cache: {e}")
170+
return None
171+
172+
logging.info(f"Loaded {len(df)} rows from trends cache.")
173+
_TRENDS_DF_CACHE = (mtime, df)
174+
return df
175+
176+
177+
def list_run_dirs(results_dir):
178+
"""Run directory names, restatted only when results_dir gains or loses entries.
179+
180+
Shared by every request in this worker, so callers must not mutate it.
181+
"""
182+
global _RUN_DIRS_CACHE
183+
184+
mtime = _mtime(results_dir)
185+
if mtime is None:
186+
return []
187+
188+
cached_mtime, dirs = _RUN_DIRS_CACHE
189+
if cached_mtime == mtime:
190+
return dirs
191+
192+
dirs = [
193+
d for d in os.listdir(results_dir)
194+
if os.path.isdir(os.path.join(results_dir, d))
195+
]
196+
_RUN_DIRS_CACHE = (mtime, dirs)
197+
return dirs
198+
199+
136200
def get_eval_details(results_dir, dir_name):
137201
details = {
138202
"product": "N/A",
@@ -228,14 +292,7 @@ def get_color_for_pct(val_str):
228292
def on_load(e: me.LoadEvent):
229293
state = me.state(State)
230294
results_dir = get_results_dir()
231-
directories = []
232-
if os.path.exists(results_dir):
233-
# List directories only
234-
directories = [
235-
d
236-
for d in os.listdir(results_dir)
237-
if os.path.isdir(os.path.join(results_dir, d))
238-
]
295+
directories = list_run_dirs(results_dir)
239296

240297
job_id = me.query_params.get("job_id") or me.query_params.get("jobid")
241298
if job_id and job_id in directories:
@@ -258,14 +315,8 @@ def on_load(e: me.LoadEvent):
258315

259316
def status_component():
260317
results_dir = get_results_dir()
261-
directories = []
262-
if os.path.exists(results_dir):
263-
directories = [
264-
d
265-
for d in os.listdir(results_dir)
266-
if os.path.isdir(os.path.join(results_dir, d))
267-
]
268-
318+
directories = list_run_dirs(results_dir)
319+
269320
with me.box(
270321
style=me.Style(
271322
background="#ffffff",
@@ -305,10 +356,9 @@ def on_agent_tab_change(e):
305356

306357
# Build summary data from precomputed trends cache
307358
data = []
308-
cache_file = os.path.join(results_dir, "trends_cache.csv")
309-
if os.path.exists(cache_file):
359+
cache_df = load_trends_df(results_dir)
360+
if cache_df is not None:
310361
try:
311-
cache_df = pd.read_csv(cache_file)
312362
for _, row in cache_df.iterrows():
313363
data.append({
314364
'AI Score': row['ai_score'] if 'ai_score' in row else None,
@@ -326,7 +376,6 @@ def on_agent_tab_change(e):
326376
except Exception as e:
327377
logging.error(f"Error reading trends cache: {e}")
328378
else:
329-
logging.warning(f"Trends cache file not found at {cache_file}")
330379
me.text("Trends cache file not found. Please run precompute.")
331380
return
332381

@@ -545,12 +594,9 @@ def _pct(value):
545594
return f"{value:.0f}%" if not pd.isna(value) else "N/A"
546595

547596

548-
def _build_summaries(cache_file):
597+
def _build_summaries(cache_df):
549598
import re
550599

551-
cache_df = pd.read_csv(cache_file)
552-
logging.info(f"Loaded {len(cache_df)} rows from trends cache.")
553-
554600
rows = []
555601
for _, row in cache_df.iterrows():
556602
score = row['ai_score'] if 'ai_score' in row else 0.0
@@ -587,21 +633,22 @@ def load_summaries(results_dir):
587633
"""
588634
global _SUMMARIES_CACHE
589635

590-
cache_file = os.path.join(results_dir, "trends_cache.csv")
591-
try:
592-
mtime = os.path.getmtime(cache_file)
593-
except OSError:
594-
logging.warning(f"Trends cache file not found at {cache_file}")
636+
mtime = _mtime(os.path.join(results_dir, "trends_cache.csv"))
637+
if mtime is None:
595638
return []
596639

597640
cached_mtime, rows = _SUMMARIES_CACHE
598641
if cached_mtime == mtime:
599642
return rows
600643

644+
cache_df = load_trends_df(results_dir)
645+
if cache_df is None:
646+
return []
647+
601648
try:
602-
rows = _build_summaries(cache_file)
649+
rows = _build_summaries(cache_df)
603650
except Exception as e:
604-
logging.error(f"Error reading trends cache: {e}")
651+
logging.error(f"Error building list rows from trends cache: {e}")
605652
return []
606653

607654
_SUMMARIES_CACHE = (mtime, rows)
@@ -2074,15 +2121,8 @@ def render_app_content():
20742121
results_dir = get_results_dir()
20752122
logging.info(f"render_app_content: selected_directory='{state.selected_directory}', selected_evals='{state.selected_evals}', selected_main_tab='{state.selected_main_tab}'")
20762123

2077-
directories = []
2078-
if os.path.exists(results_dir):
2079-
# List directories only
2080-
directories = [
2081-
d
2082-
for d in os.listdir(results_dir)
2083-
if os.path.isdir(os.path.join(results_dir, d))
2084-
]
2085-
2124+
directories = list_run_dirs(results_dir)
2125+
20862126
def on_title_click(e: me.ClickEvent):
20872127
state.selected_directory = ""
20882128
state.conversation_index = 0
@@ -2308,10 +2348,9 @@ def get_val(cfg_name):
23082348
me.text("AI Summary", type="headline-5")
23092349

23102350
if not state.ai_summary and state.selected_directory:
2311-
trends_cache_file = os.path.join(results_dir, "trends_cache.csv")
2312-
if os.path.exists(trends_cache_file):
2351+
cache_df = load_trends_df(results_dir)
2352+
if cache_df is not None:
23132353
try:
2314-
cache_df = pd.read_csv(trends_cache_file)
23152354
run_data = cache_df[cache_df['job_id'] == state.selected_directory]
23162355
if not run_data.empty and 'ai_summary' in run_data.columns:
23172356
summary = run_data['ai_summary'].values[0]

viewer/trends.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import mesop as me
44
import pandas as pd
5-
from main import State
5+
from main import State, list_run_dirs, load_trends_df
66

77
def get_results_dir():
88
# Try to read from environment variable
@@ -90,26 +90,12 @@ def trends_component():
9090
me.text(f"Results directory not found at {results_dir}")
9191
return
9292

93-
cache_file = os.path.join(results_dir, "trends_cache.csv")
94-
95-
df = None
96-
97-
# Try to load from cache
98-
if os.path.exists(cache_file):
99-
try:
100-
df = pd.read_csv(cache_file)
101-
logging.info("Loaded trends data from cache.")
102-
except Exception as e:
103-
logging.error(f"Error reading cache file: {e}")
104-
93+
df = load_trends_df(results_dir)
94+
10595
# Fallback to computing on the fly if cache is missing or failed
10696
if df is None:
107-
directories = [
108-
d
109-
for d in os.listdir(results_dir)
110-
if os.path.isdir(os.path.join(results_dir, d))
111-
]
112-
97+
directories = list_run_dirs(results_dir)
98+
11399
data = []
114100

115101
for d in directories:
@@ -333,7 +319,8 @@ def handler(e: me.ClickEvent):
333319
if state.trends_requester_filter:
334320
df = df[df['requester'] == state.trends_requester_filter]
335321

336-
df['product_dataset'] = df['product'] + " (" + df['dataset'] + ")"
322+
# assign, not item-set: df may be the shared cached frame from load_trends_df.
323+
df = df.assign(product_dataset=df['product'] + " (" + df['dataset'] + ")")
337324
df = df[df['product'].notna() & (df['product'] != 'unknown') & (df['product'].str.strip() != '')]
338325

339326
if state.trends_agent_tab == "Gemini":

0 commit comments

Comments
 (0)