From a711eeafdff8a581fbf97b93786c8ad70d33e8e0 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Wed, 3 Dec 2025 09:47:23 -0800 Subject: [PATCH] Include git versions and history function --- aocsync.py | 227 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 201 insertions(+), 26 deletions(-) diff --git a/aocsync.py b/aocsync.py index 8477f5f..85c8fc2 100755 --- a/aocsync.py +++ b/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; }} +
@@ -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"""

Part {part}

@@ -1062,6 +1188,7 @@ class HTMLGenerator: Total Time Generator Runner + Git Rev Relative Speed @@ -1089,17 +1216,22 @@ class HTMLGenerator: - - - + - """ 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'{git_rev[:7]}' + 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'
{hist_date}: {hist_time_str} ({hist_git})
') + + history_html = f''' +
+ History: + {''.join(history_items)} +
+ ''' + history_link = f'📊' + else: + history_link = "" + html += f""" - {user} + {user}{history_link} {total_str} {gen_str} {run_str} + {git_rev_html} {relative_str} +{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: