Skip to content

Commit 031b70c

Browse files
committed
Enh(viewer): Faster way to find all game folders; timestamps
1 parent 6304980 commit 031b70c

1 file changed

Lines changed: 88 additions & 66 deletions

File tree

codeclash/viewer/app.py

Lines changed: 88 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
A Flask-based web application to visualize AI agent game trajectories
66
"""
77

8+
import functools
89
import json
910
import logging
1011
import shutil
12+
import time
1113
from concurrent.futures import ThreadPoolExecutor, as_completed
1214
from dataclasses import dataclass
1315
from pathlib import Path
@@ -18,9 +20,31 @@
1820
from codeclash.analysis.significance import calculate_p_value
1921
from codeclash.tournaments.utils.git_utils import filter_git_diff, split_git_diff_by_files
2022

23+
# Configure logging
24+
logging.basicConfig(
25+
level=logging.INFO,
26+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
27+
datefmt="%H:%M:%S",
28+
)
2129
logger = logging.getLogger(__name__)
2230

2331

32+
def print_timing(func):
33+
"""Decorator to log timing information for functions/routes"""
34+
35+
@functools.wraps(func)
36+
def wrapper(*args, **kwargs):
37+
start_time = time.time()
38+
logger.info(f"Starting {func.__name__}")
39+
try:
40+
return func(*args, **kwargs)
41+
finally:
42+
elapsed = time.time() - start_time
43+
logger.info(f"Completed {func.__name__} in {elapsed:.2f}s")
44+
45+
return wrapper
46+
47+
2448
class Metadata:
2549
"""A wrapper around metadata dictionary with convenient access methods"""
2650

@@ -206,64 +230,38 @@ def _load_game_metadata(folder_info: dict[str, Any]) -> dict[str, Any]:
206230
return folder_info
207231

208232

233+
@print_timing
209234
def find_all_game_folders(base_dir: Path) -> list[dict[str, Any]]:
210-
"""Recursively find all folders and mark which ones contain metadata.json
235+
"""Find all game folders by locating metadata.json files
211236
212237
Uses parallel loading to speed up metadata.json reading for many game folders.
213238
"""
214-
all_folders = []
215-
game_folders = set() # Track which folders are actual game folders
216-
game_folder_infos = [] # Folders that need metadata loaded
217-
218-
def scan_directory(directory: Path, relative_path: str = ""):
219-
if not directory.exists() or not directory.is_dir():
220-
return
221-
222-
try:
223-
for item in directory.iterdir():
224-
if item.is_dir():
225-
current_relative = relative_path + "/" + item.name if relative_path else item.name
226-
depth = current_relative.count("/")
227-
228-
# Check if this directory is a game folder
229-
if is_game_folder(item):
230-
game_folders.add(current_relative)
231-
# Store minimal info, will load metadata in parallel later
232-
folder_info = {
233-
"name": current_relative,
234-
"full_path": str(item),
235-
"is_game": True,
236-
"depth": depth,
237-
"parent": relative_path if relative_path else None,
238-
}
239-
all_folders.append(folder_info)
240-
game_folder_infos.append(folder_info)
241-
else:
242-
# Add as intermediate folder
243-
all_folders.append(
244-
{
245-
"name": current_relative,
246-
"full_path": str(item),
247-
"round_info": None,
248-
"models": [],
249-
"game_name": "",
250-
"created_timestamp": None,
251-
"is_game": False,
252-
"depth": depth,
253-
"parent": relative_path if relative_path else None,
254-
}
255-
)
256-
257-
# Recursively scan subdirectories
258-
scan_directory(item, current_relative)
259-
except (PermissionError, OSError):
260-
# Skip directories we can't access
261-
pass
262-
263-
# First pass: scan directory structure
264-
scan_directory(base_dir)
239+
if not base_dir.exists():
240+
return []
241+
242+
# Find all metadata.json files directly
243+
metadata_files = list(base_dir.rglob("metadata.json"))
244+
245+
# Create folder info for each game folder
246+
game_folder_infos = []
247+
game_folder_paths = set()
248+
249+
for metadata_file in metadata_files:
250+
game_dir = metadata_file.parent
251+
relative_path = str(game_dir.relative_to(base_dir))
252+
game_folder_paths.add(relative_path)
253+
depth = relative_path.count("/")
254+
255+
folder_info = {
256+
"name": relative_path,
257+
"full_path": str(game_dir),
258+
"is_game": True,
259+
"depth": depth,
260+
"parent": str(game_dir.parent.relative_to(base_dir)) if game_dir.parent != base_dir else None,
261+
}
262+
game_folder_infos.append(folder_info)
265263

266-
# Second pass: load metadata.json files in parallel
264+
# Load metadata.json files in parallel
267265
if game_folder_infos:
268266
with ThreadPoolExecutor(max_workers=min(32, len(game_folder_infos))) as executor:
269267
futures = {
@@ -281,20 +279,38 @@ def scan_directory(directory: Path, relative_path: str = ""):
281279
folder_info["created_timestamp"] = None
282280
logger.warning(f"Failed to load metadata for {folder_info['name']}: {e}")
283281

284-
# Filter out intermediate folders that don't lead to any game folders
285-
filtered_folders = []
286-
for folder in sorted(all_folders, key=lambda x: x["name"]):
287-
if folder["is_game"]:
288-
# Always include game folders
289-
filtered_folders.append(folder)
290-
else:
291-
# Include intermediate folders only if they have game folders as descendants
292-
folder_path = folder["name"]
293-
has_game_descendants = any(game_path.startswith(folder_path + "/") for game_path in game_folders)
294-
if has_game_descendants:
295-
filtered_folders.append(folder)
282+
# Create intermediate folder entries for all parent directories
283+
intermediate_folders = set()
284+
for game_path in game_folder_paths:
285+
# Extract all parent paths
286+
parts = game_path.split("/")
287+
for i in range(1, len(parts)):
288+
parent_path = "/".join(parts[:i])
289+
if parent_path not in game_folder_paths: # Only add if not a game folder itself
290+
intermediate_folders.add(parent_path)
291+
292+
# Add intermediate folders to the result
293+
all_folders = game_folder_infos.copy()
294+
for intermediate_path in intermediate_folders:
295+
depth = intermediate_path.count("/")
296+
parent_parts = intermediate_path.split("/")
297+
parent_path = "/".join(parent_parts[:-1]) if len(parent_parts) > 1 else None
298+
299+
all_folders.append(
300+
{
301+
"name": intermediate_path,
302+
"full_path": str(base_dir / intermediate_path),
303+
"is_game": False,
304+
"depth": depth,
305+
"parent": parent_path,
306+
"round_info": None,
307+
"models": [],
308+
"game_name": "",
309+
"created_timestamp": None,
310+
}
311+
)
296312

297-
return filtered_folders
313+
return sorted(all_folders, key=lambda x: x["name"])
298314

299315

300316
@dataclass
@@ -820,6 +836,7 @@ def get_navigation_info(selected_folder: str) -> dict[str, str | None]:
820836
return {"previous": previous_game, "next": next_game}
821837

822838

839+
@print_timing
823840
def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
824841
"""Common logic for rendering game viewer pages"""
825842
# Parse the selected game
@@ -865,6 +882,7 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
865882

866883

867884
@app.route("/")
885+
@print_timing
868886
def index():
869887
"""Main viewer page - now redirects to picker if no folder is selected"""
870888
selected_folder = request.args.get("folder")
@@ -882,6 +900,7 @@ def index():
882900

883901

884902
@app.route("/game/<path:folder_path>")
903+
@print_timing
885904
def game_view(folder_path):
886905
"""Static-friendly game viewer route using path parameters"""
887906
# Validate the selected folder exists and is a game folder
@@ -894,6 +913,7 @@ def game_view(folder_path):
894913

895914

896915
@app.route("/picker")
916+
@print_timing
897917
def game_picker():
898918
"""Game picker page with recursive folder support"""
899919
logs_dir = LOG_BASE_DIR
@@ -1186,6 +1206,7 @@ def move_folder():
11861206

11871207

11881208
@app.route("/analysis/line-counts")
1209+
@print_timing
11891210
def analysis_line_counts():
11901211
"""Get line count analysis data for the current game"""
11911212
selected_folder = request.args.get("folder")
@@ -1277,6 +1298,7 @@ def load_log():
12771298

12781299

12791300
@app.route("/load-trajectory-diffs")
1301+
@print_timing
12801302
def load_trajectory_diffs():
12811303
"""Load trajectory diff data on demand"""
12821304
selected_folder = request.args.get("folder")

0 commit comments

Comments
 (0)