|
1 | 1 | from pathlib import Path |
2 | 2 | from collections import defaultdict |
3 | 3 | from datetime import datetime |
| 4 | +import csv |
4 | 5 | import argparse |
5 | 6 | import subprocess |
6 | 7 | import yaml |
7 | | -import csv |
8 | 8 | from jinja2 import Environment, FileSystemLoader |
9 | 9 | import logging |
10 | 10 |
|
|
16 | 16 | script_dir = Path(__file__).parent |
17 | 17 | tasks_dir = script_dir.parent / "tasks" |
18 | 18 |
|
19 | | -directories = defaultdict(dict) |
20 | | - |
21 | | -if tasks_dir.exists() and tasks_dir.is_dir(): |
22 | | - for task_name_dir in tasks_dir.iterdir(): |
23 | | - if task_name_dir.is_dir() and task_name_dir.name not in ["common"]: |
24 | | - task_name = task_name_dir.name |
25 | | - for task_type in task_types: |
26 | | - task_type_dir = task_name_dir / task_type |
27 | | - if task_type_dir.exists() and task_type_dir.is_dir(): |
28 | | - if task_name.endswith("_disabled"): |
29 | | - clean_task_name = task_name[: -len("_disabled")] |
30 | | - directories[clean_task_name][task_type] = "disabled" |
31 | | - else: |
32 | | - directories[task_name][task_type] = "done" |
33 | | - |
34 | | -config_path = Path(__file__).parent / "data" / "threads-config.yml" |
35 | | -assert config_path.exists(), f"Config file not found: {config_path}" |
36 | | -with open(config_path, "r") as file: |
37 | | - cfg = yaml.safe_load(file) |
38 | | -assert cfg, "Configuration is empty" |
39 | | -eff_num_proc = int(cfg["scoreboard"].get("efficiency", {}).get("num_proc", 1)) |
40 | | -deadlines_cfg = cfg["scoreboard"].get("deadlines", {}) |
41 | | -plagiarism_config_path = Path(__file__).parent / "data" / "plagiarism.yml" |
42 | | -with open(plagiarism_config_path, "r") as file: |
43 | | - plagiarism_cfg = yaml.safe_load(file) |
44 | | -assert plagiarism_cfg, "Plagiarism configuration is empty" |
45 | | - |
46 | | -env = Environment(loader=FileSystemLoader(Path(__file__).parent / "templates")) |
47 | | - |
48 | | - |
49 | | -perf_stat_file_path = ( |
50 | | - script_dir.parent / "build" / "perf_stat_dir" / "task_run_perf_table.csv" |
51 | | -) |
52 | | - |
53 | | -# Read and parse performance statistics CSV |
54 | | -perf_stats = dict() |
55 | | -if perf_stat_file_path.exists(): |
56 | | - with open(perf_stat_file_path, "r", newline="") as csvfile: |
57 | | - reader = csv.DictReader(csvfile) |
58 | | - for row in reader: |
59 | | - perf_stats[row["Task"]] = { |
60 | | - "seq": row["SEQ"], |
61 | | - "omp": row["OMP"], |
62 | | - "tbb": row["TBB"], |
63 | | - "stl": row["STL"], |
64 | | - "all": row["ALL"], |
65 | | - "mpi": "N/A", |
66 | | - } |
67 | | -else: |
68 | | - logger.warning("Performance stats CSV not found at %s", perf_stat_file_path) |
69 | | - |
70 | | -rows = [] |
71 | | -for dir in sorted(directories.keys()): |
72 | | - row_types = [] |
73 | | - total_count = 0 |
74 | | - for task_type in task_types: |
75 | | - max_sol_points = int(cfg["scoreboard"]["task"][task_type]["solution"]["max"]) |
76 | | - status = directories[dir].get(task_type) |
77 | | - sol_points = max_sol_points if status in ("done", "disabled") else 0 |
78 | | - solution_style = "" |
79 | | - if status == "done": |
80 | | - solution_style = "background-color: lightgreen;" |
81 | | - elif status == "disabled": |
82 | | - solution_style = "background-color: #6495ED;" |
83 | | - |
84 | | - task_points = sol_points |
85 | | - is_cheated = ( |
86 | | - dir in plagiarism_cfg["plagiarism"][task_type] |
87 | | - or dir.rstrip("_disabled") in plagiarism_cfg["plagiarism"][task_type] |
88 | | - ) |
89 | | - plagiarism_points = 0 |
90 | | - if is_cheated: |
91 | | - plag_coeff = float(cfg["scoreboard"]["plagiarism"]["coefficient"]) |
92 | | - plagiarism_points = -plag_coeff * task_points |
93 | | - task_points += plagiarism_points |
94 | | - |
95 | | - perf_val = perf_stats.get(dir, {}).get(task_type, "?") |
96 | 19 |
|
97 | | - # Calculate acceleration and efficiency if performance data is available |
98 | | - acceleration = "?" |
99 | | - efficiency = "?" |
| 20 | +def discover_tasks(tasks_dir, task_types): |
| 21 | + """Discover tasks and their implementation status from the filesystem.""" |
| 22 | + directories = defaultdict(dict) |
| 23 | + |
| 24 | + if tasks_dir.exists() and tasks_dir.is_dir(): |
| 25 | + for task_name_dir in tasks_dir.iterdir(): |
| 26 | + if task_name_dir.is_dir() and task_name_dir.name not in ["common"]: |
| 27 | + task_name = task_name_dir.name |
| 28 | + for task_type in task_types: |
| 29 | + task_type_dir = task_name_dir / task_type |
| 30 | + if task_type_dir.exists() and task_type_dir.is_dir(): |
| 31 | + if task_name.endswith("_disabled"): |
| 32 | + clean_task_name = task_name[: -len("_disabled")] |
| 33 | + directories[clean_task_name][task_type] = "disabled" |
| 34 | + else: |
| 35 | + directories[task_name][task_type] = "done" |
| 36 | + |
| 37 | + return directories |
| 38 | + |
| 39 | + |
| 40 | +directories = discover_tasks(tasks_dir, task_types) |
| 41 | + |
| 42 | + |
| 43 | +def load_performance_data(perf_stat_file_path): |
| 44 | + """Load and parse performance statistics from CSV file.""" |
| 45 | + |
| 46 | + perf_stats = dict() |
| 47 | + if perf_stat_file_path.exists(): |
| 48 | + with open(perf_stat_file_path, "r", newline="") as csvfile: |
| 49 | + reader = csv.DictReader(csvfile) |
| 50 | + for row in reader: |
| 51 | + perf_stats[row["Task"]] = { |
| 52 | + "seq": row["SEQ"], |
| 53 | + "omp": row["OMP"], |
| 54 | + "tbb": row["TBB"], |
| 55 | + "stl": row["STL"], |
| 56 | + "all": row["ALL"], |
| 57 | + "mpi": "N/A", |
| 58 | + } |
| 59 | + else: |
| 60 | + logger.warning("Performance stats CSV not found at %s", perf_stat_file_path) |
| 61 | + return perf_stats |
| 62 | + |
| 63 | + |
| 64 | +def calculate_performance_metrics(perf_val, eff_num_proc): |
| 65 | + """Calculate acceleration and efficiency from performance value.""" |
| 66 | + acceleration = "?" |
| 67 | + efficiency = "?" |
| 68 | + try: |
| 69 | + perf_float = float(perf_val) |
| 70 | + if perf_float > 0: |
| 71 | + speedup = 1.0 / perf_float |
| 72 | + acceleration = f"{speedup:.2f}" |
| 73 | + efficiency = f"{speedup / eff_num_proc * 100:.2f}%" |
| 74 | + except (ValueError, TypeError): |
| 75 | + pass |
| 76 | + return acceleration, efficiency |
| 77 | + |
| 78 | + |
| 79 | +def get_solution_points_and_style(task_type, status, cfg): |
| 80 | + """Get solution points and CSS style based on task type and status.""" |
| 81 | + max_sol_points = int(cfg["scoreboard"]["task"][task_type]["solution"]["max"]) |
| 82 | + sol_points = max_sol_points if status in ("done", "disabled") else 0 |
| 83 | + solution_style = "" |
| 84 | + if status == "done": |
| 85 | + solution_style = "background-color: lightgreen;" |
| 86 | + elif status == "disabled": |
| 87 | + solution_style = "background-color: #6495ED;" |
| 88 | + return sol_points, solution_style |
| 89 | + |
| 90 | + |
| 91 | +def check_plagiarism_and_calculate_penalty( |
| 92 | + dir, task_type, sol_points, plagiarism_cfg, cfg |
| 93 | +): |
| 94 | + """Check if task is plagiarized and calculate penalty points.""" |
| 95 | + is_cheated = ( |
| 96 | + dir in plagiarism_cfg["plagiarism"][task_type] |
| 97 | + or dir.rstrip("_disabled") in plagiarism_cfg["plagiarism"][task_type] |
| 98 | + ) |
| 99 | + plagiarism_points = 0 |
| 100 | + if is_cheated: |
| 101 | + plag_coeff = float(cfg["scoreboard"]["plagiarism"]["coefficient"]) |
| 102 | + plagiarism_points = -plag_coeff * sol_points |
| 103 | + return is_cheated, plagiarism_points |
| 104 | + |
| 105 | + |
| 106 | +def calculate_deadline_penalty(dir, task_type, status, deadlines_cfg, tasks_dir): |
| 107 | + """Calculate deadline penalty points based on git commit timestamp.""" |
| 108 | + deadline_points = 0 |
| 109 | + deadline_str = deadlines_cfg.get(task_type) |
| 110 | + if status == "done" and deadline_str: |
100 | 111 | try: |
101 | | - perf_float = float(perf_val) |
102 | | - if perf_float > 0: |
103 | | - speedup = 1.0 / perf_float |
104 | | - acceleration = f"{speedup:.2f}" |
105 | | - efficiency = f"{speedup / eff_num_proc * 100:.2f}%" |
106 | | - except (ValueError, TypeError): |
| 112 | + deadline_dt = datetime.fromisoformat(deadline_str) |
| 113 | + git_cmd = [ |
| 114 | + "git", |
| 115 | + "log", |
| 116 | + "-1", |
| 117 | + "--format=%ct", |
| 118 | + str( |
| 119 | + tasks_dir |
| 120 | + / (dir + ("_disabled" if status == "disabled" else "")) |
| 121 | + / task_type |
| 122 | + ), |
| 123 | + ] |
| 124 | + result = subprocess.run(git_cmd, capture_output=True, text=True) |
| 125 | + if result.stdout.strip().isdigit(): |
| 126 | + commit_dt = datetime.fromtimestamp(int(result.stdout.strip())) |
| 127 | + days_late = (commit_dt - deadline_dt).days |
| 128 | + if days_late > 0: |
| 129 | + deadline_points = -days_late |
| 130 | + except Exception: |
107 | 131 | pass |
| 132 | + return deadline_points |
| 133 | + |
| 134 | + |
| 135 | +def load_configurations(): |
| 136 | + """Load all configuration files and return parsed data.""" |
| 137 | + config_path = Path(__file__).parent / "data" / "threads-config.yml" |
| 138 | + assert config_path.exists(), f"Config file not found: {config_path}" |
| 139 | + with open(config_path, "r") as file: |
| 140 | + cfg = yaml.safe_load(file) |
| 141 | + assert cfg, "Configuration is empty" |
| 142 | + |
| 143 | + eff_num_proc = int(cfg["scoreboard"].get("efficiency", {}).get("num_proc", 1)) |
| 144 | + deadlines_cfg = cfg["scoreboard"].get("deadlines", {}) |
| 145 | + |
| 146 | + plagiarism_config_path = Path(__file__).parent / "data" / "plagiarism.yml" |
| 147 | + with open(plagiarism_config_path, "r") as file: |
| 148 | + plagiarism_cfg = yaml.safe_load(file) |
| 149 | + assert plagiarism_cfg, "Plagiarism configuration is empty" |
| 150 | + |
| 151 | + return cfg, eff_num_proc, deadlines_cfg, plagiarism_cfg |
| 152 | + |
| 153 | + |
| 154 | +def main(): |
| 155 | + """Main function to generate the scoreboard.""" |
| 156 | + cfg, eff_num_proc, deadlines_cfg, plagiarism_cfg = load_configurations() |
| 157 | + |
| 158 | + env = Environment(loader=FileSystemLoader(Path(__file__).parent / "templates")) |
| 159 | + |
| 160 | + perf_stat_file_path = ( |
| 161 | + script_dir.parent / "build" / "perf_stat_dir" / "task_run_perf_table.csv" |
| 162 | + ) |
| 163 | + |
| 164 | + # Read and parse performance statistics CSV |
| 165 | + perf_stats = load_performance_data(perf_stat_file_path) |
| 166 | + |
| 167 | + rows = [] |
| 168 | + for dir in sorted(directories.keys()): |
| 169 | + row_types = [] |
| 170 | + total_count = 0 |
| 171 | + for task_type in task_types: |
| 172 | + status = directories[dir].get(task_type) |
| 173 | + sol_points, solution_style = get_solution_points_and_style( |
| 174 | + task_type, status, cfg |
| 175 | + ) |
| 176 | + |
| 177 | + task_points = sol_points |
| 178 | + is_cheated, plagiarism_points = check_plagiarism_and_calculate_penalty( |
| 179 | + dir, task_type, sol_points, plagiarism_cfg, cfg |
| 180 | + ) |
| 181 | + task_points += plagiarism_points |
| 182 | + |
| 183 | + perf_val = perf_stats.get(dir, {}).get(task_type, "?") |
| 184 | + |
| 185 | + # Calculate acceleration and efficiency if performance data is available |
| 186 | + acceleration, efficiency = calculate_performance_metrics( |
| 187 | + perf_val, eff_num_proc |
| 188 | + ) |
| 189 | + |
| 190 | + # Calculate deadline penalty points |
| 191 | + deadline_points = calculate_deadline_penalty( |
| 192 | + dir, task_type, status, deadlines_cfg, tasks_dir |
| 193 | + ) |
| 194 | + |
| 195 | + row_types.append( |
| 196 | + { |
| 197 | + "solution_points": sol_points, |
| 198 | + "solution_style": solution_style, |
| 199 | + "perf": perf_val, |
| 200 | + "acceleration": acceleration, |
| 201 | + "efficiency": efficiency, |
| 202 | + "deadline_points": deadline_points, |
| 203 | + "plagiarised": is_cheated, |
| 204 | + "plagiarism_points": plagiarism_points, |
| 205 | + } |
| 206 | + ) |
| 207 | + total_count += task_points |
| 208 | + |
| 209 | + rows.append({"task": dir, "types": row_types, "total": total_count}) |
| 210 | + |
| 211 | + template = env.get_template("index.html.j2") |
| 212 | + html_content = template.render(task_types=task_types, rows=rows) |
| 213 | + |
| 214 | + parser = argparse.ArgumentParser(description="Generate HTML scoreboard.") |
| 215 | + parser.add_argument( |
| 216 | + "-o", "--output", type=str, required=True, help="Output file path" |
| 217 | + ) |
| 218 | + args = parser.parse_args() |
| 219 | + |
| 220 | + output_file = Path(args.output) / "index.html" |
| 221 | + with open(output_file, "w") as file: |
| 222 | + file.write(html_content) |
| 223 | + |
| 224 | + logger.info("HTML page generated at %s", output_file) |
| 225 | + |
108 | 226 |
|
109 | | - # Calculate deadline penalty points |
110 | | - deadline_points = 0 |
111 | | - deadline_str = deadlines_cfg.get(task_type) |
112 | | - if status == "done" and deadline_str: |
113 | | - try: |
114 | | - deadline_dt = datetime.fromisoformat(deadline_str) |
115 | | - git_cmd = [ |
116 | | - "git", |
117 | | - "log", |
118 | | - "-1", |
119 | | - "--format=%ct", |
120 | | - str( |
121 | | - tasks_dir |
122 | | - / (dir + ("_disabled" if status == "disabled" else "")) |
123 | | - / task_type |
124 | | - ), |
125 | | - ] |
126 | | - result = subprocess.run(git_cmd, capture_output=True, text=True) |
127 | | - if result.stdout.strip().isdigit(): |
128 | | - commit_dt = datetime.fromtimestamp(int(result.stdout.strip())) |
129 | | - days_late = (commit_dt - deadline_dt).days |
130 | | - if days_late > 0: |
131 | | - deadline_points = -days_late |
132 | | - except Exception: |
133 | | - pass |
134 | | - |
135 | | - row_types.append( |
136 | | - { |
137 | | - "solution_points": sol_points, |
138 | | - "solution_style": solution_style, |
139 | | - "perf": perf_val, |
140 | | - "acceleration": acceleration, |
141 | | - "efficiency": efficiency, |
142 | | - "deadline_points": deadline_points, |
143 | | - "plagiarised": is_cheated, |
144 | | - "plagiarism_points": plagiarism_points, |
145 | | - } |
146 | | - ) |
147 | | - total_count += task_points |
148 | | - |
149 | | - rows.append({"task": dir, "types": row_types, "total": total_count}) |
150 | | - |
151 | | -template = env.get_template("index.html.j2") |
152 | | -html_content = template.render(task_types=task_types, rows=rows) |
153 | | - |
154 | | -parser = argparse.ArgumentParser(description="Generate HTML scoreboard.") |
155 | | -parser.add_argument("-o", "--output", type=str, required=True, help="Output file path") |
156 | | -args = parser.parse_args() |
157 | | - |
158 | | -output_file = Path(args.output) / "index.html" |
159 | | -with open(output_file, "w") as file: |
160 | | - file.write(html_content) |
161 | | - |
162 | | -logger.info("HTML page generated at %s", output_file) |
| 227 | +if __name__ == "__main__": |
| 228 | + main() |
0 commit comments