@@ -700,13 +700,13 @@ def _compute_display_deadlines_processes(n_items: int) -> list[date]:
700700 (p for p in candidates_processes if p .exists ()), candidates_processes [0 ]
701701 )
702702
703- # Read and merge performance statistics CSVs
703+ # Read and merge performance statistics CSVs (keys = CSV Task column)
704704 perf_stats_threads = load_performance_data_threads (threads_csv )
705705 perf_stats_processes = load_performance_data_processes (processes_csv )
706- perf_stats : dict [str , dict ] = {}
707- perf_stats .update (perf_stats_threads )
706+ perf_stats_raw : dict [str , dict ] = {}
707+ perf_stats_raw .update (perf_stats_threads )
708708 for k , v in perf_stats_processes .items ():
709- perf_stats [k ] = {** perf_stats .get (k , {}), ** v }
709+ perf_stats_raw [k ] = {** perf_stats_raw .get (k , {}), ** v }
710710
711711 # Partition tasks by tasks_type from settings.json
712712 threads_task_dirs = [
@@ -724,6 +724,73 @@ def _compute_display_deadlines_processes(n_items: int) -> list[date]:
724724 elif "processes" in name :
725725 processes_task_dirs .append (name )
726726
727+ # Resolve performance stats keys (from CSV Task names) to actual task directories
728+ import re as _re
729+
730+ def _family_from_name (name : str ) -> tuple [str , int ]:
731+ # Infer family from CSV Task value, using only structural markers
732+ # threads -> ("threads", 0); processes[_N] -> ("processes", N|1)
733+ if "threads" in name :
734+ return "threads" , 0
735+ if "processes" in name :
736+ m = _re .search (r"processes(?:_(\d+))?" , name )
737+ if m :
738+ try :
739+ idx = int (m .group (1 )) if m .group (1 ) else 1
740+ except Exception :
741+ idx = 1
742+ else :
743+ idx = 1
744+ return "processes" , idx
745+ # Fallback: treat as threads family
746+ return "threads" , 0
747+
748+ def _family_from_dir (dir_name : str ) -> tuple [str , int ]:
749+ # Prefer explicit tasks_type from settings.json and task_number from info.json
750+ kind_guess = tasks_type_map .get (dir_name ) or (
751+ "threads" if "threads" in dir_name else "processes"
752+ )
753+ idx = 0
754+ if kind_guess == "processes" :
755+ # Lightweight reader to avoid dependency on later-scoped helpers
756+ try :
757+ import json as _json
758+
759+ info_path = tasks_dir / dir_name / "info.json"
760+ if info_path .exists ():
761+ with open (info_path , "r" ) as _f :
762+ data = _json .load (_f )
763+ s = data .get ("student" , {}) if isinstance (data , dict ) else {}
764+ try :
765+ idx = int (str (s .get ("task_number" , "0" )))
766+ except Exception :
767+ idx = 0
768+ except Exception :
769+ idx = 0
770+ return kind_guess , idx
771+
772+ # Build map family -> list of dir names in this repo
773+ family_to_dirs : dict [tuple [str , int ], list [str ]] = {}
774+ for d in sorted (directories .keys ()):
775+ fam = _family_from_dir (d )
776+ family_to_dirs .setdefault (fam , []).append (d )
777+
778+ # Aggregate perf by family (CSV keys may not match dir names)
779+ perf_by_family : dict [tuple [str , int ], dict ] = {}
780+ for key , vals in perf_stats_raw .items ():
781+ fam = _family_from_name (key )
782+ perf_by_family [fam ] = {** perf_by_family .get (fam , {}), ** vals }
783+
784+ # Project family perf onto actual directories (prefer exact one per family)
785+ perf_stats : dict [str , dict ] = {}
786+ for fam , vals in perf_by_family .items ():
787+ dirs_for_family = family_to_dirs .get (fam , [])
788+ if not dirs_for_family :
789+ continue
790+ # Assign same perf to all dirs in the family (usually one)
791+ for d in dirs_for_family :
792+ perf_stats [d ] = vals .copy ()
793+
727794 # Build rows for each page
728795 threads_rows = _build_rows_for_task_types (
729796 task_types_threads ,
@@ -758,15 +825,15 @@ def _identity_key(student: dict) -> str:
758825 ]
759826 )
760827
761- def _build_cell (dir_name : str , ttype : str ):
828+ def _build_cell (dir_name : str , ttype : str , perf_map : dict [ str , dict ] ):
762829 status = directories [dir_name ].get (ttype )
763830 sol_points , solution_style = get_solution_points_and_style (ttype , status , cfg )
764831 task_points = sol_points
765832 is_cheated , plagiarism_points = check_plagiarism_and_calculate_penalty (
766833 dir_name , ttype , sol_points , plagiarism_cfg , cfg , semester = "processes"
767834 )
768835 task_points += plagiarism_points
769- perf_val = perf_stats .get (dir_name , {}).get (ttype , "?" )
836+ perf_val = perf_map .get (dir_name , {}).get (ttype , "?" )
770837 acceleration , efficiency = calculate_performance_metrics (
771838 perf_val , eff_num_proc , ttype
772839 )
@@ -832,7 +899,7 @@ def _build_cell(dir_name: str, ttype: str):
832899 proc_group_headers .append ({"type" : "seq" })
833900 group_cells = []
834901 for ttype in ["mpi" , "seq" ]:
835- cell , _ = _build_cell (d , ttype )
902+ cell , _ = _build_cell (d , ttype , perf_stats )
836903 group_cells .append (cell )
837904 # Override displayed points for processes: S under MPI/SEQ from points-info; A points under MPI only
838905 s_mpi , s_seq , a_mpi , r_max = _find_process_points (cfg , n )
@@ -948,6 +1015,16 @@ def _build_cell(dir_name: str, ttype: str):
9481015 }
9491016 ]
9501017
1018+ # Rebuild threads rows with resolved perf stats
1019+ threads_rows = _build_rows_for_task_types (
1020+ task_types_threads ,
1021+ threads_task_dirs ,
1022+ perf_stats ,
1023+ cfg ,
1024+ eff_num_proc ,
1025+ deadlines_cfg ,
1026+ )
1027+
9511028 parser = argparse .ArgumentParser (description = "Generate HTML scoreboard." )
9521029 parser .add_argument (
9531030 "-o" , "--output" , type = str , required = True , help = "Output directory path"
0 commit comments