Include git versions and history function

This commit is contained in:
Bill Thiede 2025-12-03 09:47:23 -08:00
parent ac85c2defa
commit a711eeafdf

View File

@ -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: