Include git versions and history function
This commit is contained in:
parent
ac85c2defa
commit
a711eeafdf
227
aocsync.py
227
aocsync.py
@ -37,6 +37,8 @@ class PerformanceResult:
|
||||
part: int
|
||||
time_ns: int # Runner time in nanoseconds
|
||||
generator_time_ns: int = 0 # Generator time in nanoseconds (optional)
|
||||
git_rev: str = "" # Git revision (short hash)
|
||||
repo_url: str = "" # Repository URL
|
||||
timestamp: str = ""
|
||||
|
||||
|
||||
@ -107,17 +109,24 @@ class Database:
|
||||
part INTEGER NOT NULL,
|
||||
time_ns INTEGER NOT NULL,
|
||||
generator_time_ns INTEGER NOT NULL DEFAULT 0,
|
||||
git_rev TEXT NOT NULL DEFAULT '',
|
||||
repo_url TEXT NOT NULL DEFAULT '',
|
||||
timestamp TEXT NOT NULL,
|
||||
UNIQUE(user, year, day, part, timestamp)
|
||||
UNIQUE(user, year, day, part, timestamp, git_rev)
|
||||
)
|
||||
''')
|
||||
|
||||
# Add generator_time_ns column if it doesn't exist (for existing databases)
|
||||
try:
|
||||
cursor.execute('ALTER TABLE results ADD COLUMN generator_time_ns INTEGER NOT NULL DEFAULT 0')
|
||||
except sqlite3.OperationalError:
|
||||
# Column already exists
|
||||
pass
|
||||
# Add new columns if they don't exist (for existing databases)
|
||||
for column, col_type in [
|
||||
('generator_time_ns', 'INTEGER NOT NULL DEFAULT 0'),
|
||||
('git_rev', 'TEXT NOT NULL DEFAULT \'\''),
|
||||
('repo_url', 'TEXT NOT NULL DEFAULT \'\'')
|
||||
]:
|
||||
try:
|
||||
cursor.execute(f'ALTER TABLE results ADD COLUMN {column} {col_type}')
|
||||
except sqlite3.OperationalError:
|
||||
# Column already exists
|
||||
pass
|
||||
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_user_year_day_part
|
||||
@ -135,10 +144,11 @@ class Database:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO results
|
||||
(user, year, day, part, time_ns, generator_time_ns, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(user, year, day, part, time_ns, generator_time_ns, git_rev, repo_url, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (result.user, result.year, result.day, result.part,
|
||||
result.time_ns, result.generator_time_ns, result.timestamp))
|
||||
result.time_ns, result.generator_time_ns, result.git_rev,
|
||||
result.repo_url, result.timestamp))
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
# Already exists, skip
|
||||
@ -156,7 +166,7 @@ class Database:
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = '''
|
||||
SELECT user, year, day, part, time_ns, generator_time_ns, timestamp
|
||||
SELECT user, year, day, part, time_ns, generator_time_ns, git_rev, repo_url, timestamp
|
||||
FROM results r1
|
||||
WHERE timestamp = (
|
||||
SELECT MAX(timestamp)
|
||||
@ -198,7 +208,35 @@ class Database:
|
||||
'part': row[3],
|
||||
'time_ns': row[4],
|
||||
'generator_time_ns': row[5] if len(row) > 5 else 0,
|
||||
'timestamp': row[6] if len(row) > 6 else row[5]
|
||||
'git_rev': row[6] if len(row) > 6 else '',
|
||||
'repo_url': row[7] if len(row) > 7 else '',
|
||||
'timestamp': row[8] if len(row) > 8 else (row[6] if len(row) > 6 else '')
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def get_historical_results(self, user: str, year: int, day: int, part: int) -> List[Dict]:
|
||||
"""Get historical results for a specific user/day/part"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT time_ns, generator_time_ns, git_rev, repo_url, timestamp
|
||||
FROM results
|
||||
WHERE user = ? AND year = ? AND day = ? AND part = ?
|
||||
ORDER BY timestamp DESC
|
||||
''', (user, year, day, part))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
{
|
||||
'time_ns': row[0],
|
||||
'generator_time_ns': row[1],
|
||||
'git_rev': row[2],
|
||||
'repo_url': row[3],
|
||||
'timestamp': row[4]
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
@ -388,20 +426,41 @@ class CargoAOCRunner:
|
||||
|
||||
return sorted(years) if years else []
|
||||
|
||||
@staticmethod
|
||||
def get_git_rev(repo_path: Path) -> str:
|
||||
"""Get short git revision hash"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get git rev for {repo_path}: {e}")
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def run_benchmarks(repo_path: Path, year: int, user: str = "unknown",
|
||||
is_multi_year: bool = False) -> List[PerformanceResult]:
|
||||
repo_url: str = "", is_multi_year: bool = False) -> List[PerformanceResult]:
|
||||
"""Run cargo aoc benchmarks and parse results
|
||||
|
||||
Args:
|
||||
repo_path: Path to the repository root (for single repos) or year directory (for multi-year repos)
|
||||
year: The year to benchmark
|
||||
user: User name for the results
|
||||
repo_url: Repository URL for linking
|
||||
is_multi_year: True if this is a multi-year repo (repo_path is already the year directory)
|
||||
"""
|
||||
results = []
|
||||
repo_path = Path(repo_path)
|
||||
|
||||
# Get git revision
|
||||
git_rev = CargoAOCRunner.get_git_rev(repo_path)
|
||||
|
||||
# Determine the working directory
|
||||
if is_multi_year:
|
||||
# For multi-year repos, repo_path is already the year directory
|
||||
@ -413,6 +472,8 @@ class CargoAOCRunner:
|
||||
if year_dir.exists() and year_dir.is_dir():
|
||||
work_dir = year_dir
|
||||
logger.info(f"Using year directory: {work_dir}")
|
||||
# Get git rev from repo root, not year directory
|
||||
git_rev = CargoAOCRunner.get_git_rev(repo_path)
|
||||
|
||||
if not (work_dir / 'Cargo.toml').exists():
|
||||
logger.warning(f"No Cargo.toml found in {work_dir}")
|
||||
@ -445,7 +506,7 @@ class CargoAOCRunner:
|
||||
|
||||
# Parse output for runtime information
|
||||
day_results = CargoAOCRunner._parse_runtime_output(
|
||||
result.stdout, result.stderr, day, year, user
|
||||
result.stdout, result.stderr, day, year, user, git_rev, repo_url
|
||||
)
|
||||
if day_results:
|
||||
logger.info(f"Parsed {len(day_results)} runtime result(s) for {user} year {year} day {day}")
|
||||
@ -464,7 +525,7 @@ class CargoAOCRunner:
|
||||
|
||||
@staticmethod
|
||||
def _parse_runtime_output(stdout: str, stderr: str, day: int, year: int,
|
||||
user: str) -> List[PerformanceResult]:
|
||||
user: str, git_rev: str = "", repo_url: str = "") -> List[PerformanceResult]:
|
||||
"""Parse cargo-aoc runtime output
|
||||
|
||||
cargo aoc typically outputs timing information like:
|
||||
@ -537,6 +598,8 @@ class CargoAOCRunner:
|
||||
part=current_part,
|
||||
time_ns=runner_time_ns,
|
||||
generator_time_ns=generator_time_ns,
|
||||
git_rev=git_rev,
|
||||
repo_url=repo_url,
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
@ -579,6 +642,8 @@ class CargoAOCRunner:
|
||||
part=current_part,
|
||||
time_ns=runner_time_ns,
|
||||
generator_time_ns=generator_time_ns,
|
||||
git_rev=git_rev,
|
||||
repo_url=repo_url,
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
@ -744,6 +809,8 @@ class HTMLGenerator:
|
||||
user = result['user']
|
||||
runner_time_ns = result['time_ns']
|
||||
generator_time_ns = result.get('generator_time_ns', 0)
|
||||
git_rev = result.get('git_rev', '')
|
||||
repo_url = result.get('repo_url', '')
|
||||
|
||||
# Only store if runner_time_ns > 0 (valid result)
|
||||
# Store total time (generator + runner) for comparison
|
||||
@ -752,10 +819,12 @@ class HTMLGenerator:
|
||||
data[year][day][part][user] = {
|
||||
'total': total_time_ns,
|
||||
'runner': runner_time_ns,
|
||||
'generator': generator_time_ns
|
||||
'generator': generator_time_ns,
|
||||
'git_rev': git_rev,
|
||||
'repo_url': repo_url
|
||||
}
|
||||
|
||||
html = self._generate_html(data, years, users)
|
||||
html = self._generate_html(data, years, users, db)
|
||||
|
||||
output_file = self.output_dir / 'index.html'
|
||||
with open(output_file, 'w') as f:
|
||||
@ -763,7 +832,7 @@ class HTMLGenerator:
|
||||
|
||||
logger.info(f"Generated HTML report: {output_file}")
|
||||
|
||||
def _generate_html(self, data: dict, years: List[int], users: List[str]) -> str:
|
||||
def _generate_html(self, data: dict, years: List[int], users: List[str], db: Database) -> str:
|
||||
"""Generate HTML content"""
|
||||
# Sort years descending (most recent first)
|
||||
sorted_years = sorted(years, reverse=True)
|
||||
@ -932,6 +1001,15 @@ class HTMLGenerator:
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
a {{
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}}
|
||||
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
|
||||
.fastest {{
|
||||
background: #d4edda !important;
|
||||
color: #155724;
|
||||
@ -947,6 +1025,40 @@ class HTMLGenerator:
|
||||
font-style: italic;
|
||||
}}
|
||||
|
||||
.history-link {{
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
|
||||
.history-link:hover {{
|
||||
color: #667eea;
|
||||
text-decoration: underline;
|
||||
}}
|
||||
|
||||
.history-data {{
|
||||
display: none;
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
}}
|
||||
|
||||
.history-data.show {{
|
||||
display: block;
|
||||
}}
|
||||
|
||||
.history-item {{
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}}
|
||||
|
||||
.history-item:last-child {{
|
||||
border-bottom: none;
|
||||
}}
|
||||
|
||||
.summary {{
|
||||
margin-top: 30px;
|
||||
padding: 15px;
|
||||
@ -987,6 +1099,15 @@ class HTMLGenerator:
|
||||
color: #333;
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
function toggleHistory(user, year, day, part) {{
|
||||
const id = `history-${{user}}-${{year}}-${{day}}-${{part}}`;
|
||||
const elem = document.getElementById(id);
|
||||
if (elem) {{
|
||||
elem.classList.toggle('show');
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@ -1052,6 +1173,11 @@ class HTMLGenerator:
|
||||
fastest_time = times[0][2]
|
||||
slowest_time = times[-1][2]
|
||||
|
||||
# Get git rev and repo_url for historical data link
|
||||
first_user_data = times[0][1] if isinstance(times[0][1], dict) else {}
|
||||
sample_git_rev = first_user_data.get('git_rev', '')
|
||||
sample_repo_url = first_user_data.get('repo_url', '')
|
||||
|
||||
html += f"""
|
||||
<div class="part-section">
|
||||
<h4 class="part-header">Part {part}</h4>
|
||||
@ -1062,6 +1188,7 @@ class HTMLGenerator:
|
||||
<th>Total Time</th>
|
||||
<th>Generator</th>
|
||||
<th>Runner</th>
|
||||
<th>Git Rev</th>
|
||||
<th>Relative Speed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -1089,17 +1216,22 @@ class HTMLGenerator:
|
||||
<td class="no-data">-</td>
|
||||
<td class="no-data">-</td>
|
||||
<td class="no-data">-</td>
|
||||
<td class="no-data">-</td>
|
||||
</tr>
|
||||
"""
|
||||
else:
|
||||
# Extract times
|
||||
# Extract times and metadata
|
||||
if isinstance(time_data, dict):
|
||||
runner_time_ns = time_data.get('runner', 0)
|
||||
generator_time_ns = time_data.get('generator', 0)
|
||||
git_rev = time_data.get('git_rev', '')
|
||||
repo_url = time_data.get('repo_url', '')
|
||||
else:
|
||||
# Backward compatibility
|
||||
runner_time_ns = total_time_ns
|
||||
generator_time_ns = 0
|
||||
git_rev = ''
|
||||
repo_url = ''
|
||||
|
||||
# Format total time
|
||||
total_ms = total_time_ns / 1_000_000
|
||||
@ -1147,14 +1279,56 @@ class HTMLGenerator:
|
||||
elif total_time_ns == slowest_time and len(times) > 1:
|
||||
row_class = "slowest"
|
||||
|
||||
# Format git rev with link if available
|
||||
if git_rev and repo_url:
|
||||
# Create link to commit
|
||||
commit_url = repo_url.rstrip('/') + '/commit/' + git_rev
|
||||
git_rev_html = f'<a href="{commit_url}" target="_blank" title="View commit">{git_rev[:7]}</a>'
|
||||
elif git_rev:
|
||||
git_rev_html = git_rev[:7]
|
||||
else:
|
||||
git_rev_html = '-'
|
||||
|
||||
# Get historical data for this user/day/part
|
||||
historical = db.get_historical_results(user, year, day, part)
|
||||
history_html = ""
|
||||
if len(historical) > 1:
|
||||
history_items = []
|
||||
for hist in historical[:10]: # Show last 10 runs
|
||||
hist_total = hist['time_ns'] + hist.get('generator_time_ns', 0)
|
||||
hist_ms = hist_total / 1_000_000
|
||||
hist_us = hist_total / 1_000
|
||||
if hist_ms >= 1:
|
||||
hist_time_str = f"{hist_ms:.2f} ms"
|
||||
elif hist_us >= 1:
|
||||
hist_time_str = f"{hist_us:.2f} μs"
|
||||
else:
|
||||
hist_time_str = f"{hist_total} ns"
|
||||
|
||||
hist_git = hist.get('git_rev', '')[:7] if hist.get('git_rev') else '-'
|
||||
hist_date = hist.get('timestamp', '')[:16] if hist.get('timestamp') else ''
|
||||
history_items.append(f'<div class="history-item">{hist_date}: {hist_time_str} ({hist_git})</div>')
|
||||
|
||||
history_html = f'''
|
||||
<div class="history-data" id="history-{user}-{year}-{day}-{part}">
|
||||
<strong>History:</strong>
|
||||
{''.join(history_items)}
|
||||
</div>
|
||||
'''
|
||||
history_link = f'<span class="history-link" onclick="toggleHistory(\'{user}\', {year}, {day}, {part})">📊</span>'
|
||||
else:
|
||||
history_link = ""
|
||||
|
||||
html += f"""
|
||||
<tr class="{row_class}">
|
||||
<td>{user}</td>
|
||||
<td>{user}{history_link}</td>
|
||||
<td class="time">{total_str}</td>
|
||||
<td class="time">{gen_str}</td>
|
||||
<td class="time">{run_str}</td>
|
||||
<td>{git_rev_html}</td>
|
||||
<td>{relative_str}</td>
|
||||
</tr>
|
||||
{history_html}
|
||||
"""
|
||||
|
||||
html += """
|
||||
@ -1235,11 +1409,12 @@ class AOCSync:
|
||||
# Check if years are specified in config
|
||||
config_years = repo_config.get('years')
|
||||
|
||||
url = repo_config['url']
|
||||
if config_years:
|
||||
# Use years from config
|
||||
for year in config_years:
|
||||
self._run_and_store_benchmarks(repo_path, year, user_name,
|
||||
is_multi_year=False)
|
||||
repo_url=url, is_multi_year=False)
|
||||
else:
|
||||
# Try to determine year(s) from the repository
|
||||
years = CargoAOCRunner.extract_years_from_repo(repo_path)
|
||||
@ -1248,7 +1423,7 @@ class AOCSync:
|
||||
# Run benchmarks for each detected year
|
||||
for year in years:
|
||||
self._run_and_store_benchmarks(repo_path, year, user_name,
|
||||
is_multi_year=False)
|
||||
repo_url=url, is_multi_year=False)
|
||||
else:
|
||||
# If no year detected, check for year directories
|
||||
logger.warning(f"No year detected for {user_name}, checking for year directories")
|
||||
@ -1258,7 +1433,7 @@ class AOCSync:
|
||||
if year_dir.exists() and year_dir.is_dir():
|
||||
logger.info(f"Found year directory {try_year} for {user_name}")
|
||||
self._run_and_store_benchmarks(repo_path, try_year, user_name,
|
||||
is_multi_year=False)
|
||||
repo_url=url, is_multi_year=False)
|
||||
|
||||
elif repo_type == 'multi-year':
|
||||
# Multiple repositories, one per year
|
||||
@ -1276,7 +1451,7 @@ class AOCSync:
|
||||
if self.git_manager.clone_or_update_repo(url, local_path):
|
||||
repo_path = Path(local_path)
|
||||
self._run_and_store_benchmarks(repo_path, year, user_name,
|
||||
is_multi_year=True)
|
||||
repo_url=url, is_multi_year=True)
|
||||
|
||||
def _check_year_in_repo(self, repo_path: Path, year: int) -> bool:
|
||||
"""Check if a repository contains solutions for a specific year"""
|
||||
@ -1295,11 +1470,11 @@ class AOCSync:
|
||||
return False
|
||||
|
||||
def _run_and_store_benchmarks(self, repo_path: Path, year: int, user: str,
|
||||
is_multi_year: bool = False):
|
||||
repo_url: str = "", is_multi_year: bool = False):
|
||||
"""Run benchmarks and store results"""
|
||||
logger.info(f"Running benchmarks for {user} year {year} in {repo_path}")
|
||||
results = CargoAOCRunner.run_benchmarks(repo_path, year=year, user=user,
|
||||
is_multi_year=is_multi_year)
|
||||
repo_url=repo_url, is_multi_year=is_multi_year)
|
||||
|
||||
# Store results
|
||||
for result in results:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user