88import json
99import logging
1010import shutil
11+ from concurrent .futures import ThreadPoolExecutor , as_completed
1112from dataclasses import dataclass
1213from pathlib import Path
1314from typing import Any
@@ -194,10 +195,25 @@ def get_agent_info_from_metadata(metadata: Metadata) -> list[AgentInfo]:
194195 return agents
195196
196197
198+ def _load_game_metadata (folder_info : dict [str , Any ]) -> dict [str , Any ]:
199+ """Helper function to load metadata for a single game folder in parallel"""
200+ item = Path (folder_info ["full_path" ])
201+ metadata = load_metadata (item )
202+ folder_info ["round_info" ] = metadata .round_count_info
203+ folder_info ["models" ] = metadata .models
204+ folder_info ["game_name" ] = metadata .game_name
205+ folder_info ["created_timestamp" ] = metadata .get_path ("created_timestamp" )
206+ return folder_info
207+
208+
197209def find_all_game_folders (base_dir : Path ) -> list [dict [str , Any ]]:
198- """Recursively find all folders and mark which ones contain metadata.json"""
210+ """Recursively find all folders and mark which ones contain metadata.json
211+
212+ Uses parallel loading to speed up metadata.json reading for many game folders.
213+ """
199214 all_folders = []
200215 game_folders = set () # Track which folders are actual game folders
216+ game_folder_infos = [] # Folders that need metadata loaded
201217
202218 def scan_directory (directory : Path , relative_path : str = "" ):
203219 if not directory .exists () or not directory .is_dir ():
@@ -207,33 +223,23 @@ def scan_directory(directory: Path, relative_path: str = ""):
207223 for item in directory .iterdir ():
208224 if item .is_dir ():
209225 current_relative = relative_path + "/" + item .name if relative_path else item .name
210-
211226 depth = current_relative .count ("/" )
212227
213228 # Check if this directory is a game folder
214229 if is_game_folder (item ):
215- # Load metadata once
216- metadata = load_metadata (item )
217- round_info = metadata .round_count_info
218- models = metadata .models
219- game_name = metadata .game_name
220- created_timestamp = metadata .get_path ("created_timestamp" )
221230 game_folders .add (current_relative )
222- all_folders .append (
223- {
224- "name" : current_relative ,
225- "full_path" : str (item ),
226- "round_info" : round_info , # Now stores (completed, total) tuple or None
227- "models" : models ,
228- "game_name" : game_name ,
229- "created_timestamp" : created_timestamp ,
230- "is_game" : True ,
231- "depth" : depth ,
232- "parent" : relative_path if relative_path else None ,
233- }
234- )
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 )
235241 else :
236- # Add as intermediate folder if it has game folders in subdirectories
242+ # Add as intermediate folder
237243 all_folders .append (
238244 {
239245 "name" : current_relative ,
@@ -254,8 +260,27 @@ def scan_directory(directory: Path, relative_path: str = ""):
254260 # Skip directories we can't access
255261 pass
256262
263+ # First pass: scan directory structure
257264 scan_directory (base_dir )
258265
266+ # Second pass: load metadata.json files in parallel
267+ if game_folder_infos :
268+ with ThreadPoolExecutor (max_workers = min (32 , len (game_folder_infos ))) as executor :
269+ futures = {
270+ executor .submit (_load_game_metadata , folder_info ): folder_info for folder_info in game_folder_infos
271+ }
272+ for future in as_completed (futures ):
273+ try :
274+ future .result () # Updates folder_info in place
275+ except Exception as e :
276+ # If metadata loading fails, set default values
277+ folder_info = futures [future ]
278+ folder_info ["round_info" ] = None
279+ folder_info ["models" ] = []
280+ folder_info ["game_name" ] = ""
281+ folder_info ["created_timestamp" ] = None
282+ logger .warning (f"Failed to load metadata for { folder_info ['name' ]} : { e } " )
283+
259284 # Filter out intermediate folders that don't lead to any game folders
260285 filtered_folders = []
261286 for folder in sorted (all_folders , key = lambda x : x ["name" ]):
0 commit comments