Skip to content

Commit 3bae7f2

Browse files
authored
[scoreboard] Provide a better code structure (extract to functions) (#544)
1 parent fd454f0 commit 3bae7f2

File tree

1 file changed

+207
-141
lines changed

1 file changed

+207
-141
lines changed

scoreboard/main.py

Lines changed: 207 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from pathlib import Path
22
from collections import defaultdict
33
from datetime import datetime
4+
import csv
45
import argparse
56
import subprocess
67
import yaml
7-
import csv
88
from jinja2 import Environment, FileSystemLoader
99
import logging
1010

@@ -16,147 +16,213 @@
1616
script_dir = Path(__file__).parent
1717
tasks_dir = script_dir.parent / "tasks"
1818

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, "?")
9619

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:
100111
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:
107131
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+
108226

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

Comments
 (0)