Just run cargo aoc instead of cargo aoc bench

This commit is contained in:
Bill Thiede 2025-12-02 15:55:54 -08:00
parent 6960863b24
commit 20c04b32bb
2 changed files with 152 additions and 111 deletions

View File

@ -6,7 +6,7 @@ A Python script that polls multiple git repositories containing Advent of Code i
- **Automatic Git Polling**: Monitors multiple repositories for changes - **Automatic Git Polling**: Monitors multiple repositories for changes
- **Flexible Repository Structure**: Supports both single-repo (all years) and multi-repo (one per year) configurations - **Flexible Repository Structure**: Supports both single-repo (all years) and multi-repo (one per year) configurations
- **Automatic Benchmarking**: Runs `cargo aoc bench` for all implemented days - **Automatic Runtime Measurement**: Runs `cargo aoc` for all implemented days and parses runtime information
- **Performance Parsing**: Extracts timing data from cargo-aoc output - **Performance Parsing**: Extracts timing data from cargo-aoc output
- **Data Storage**: SQLite database for historical performance data - **Data Storage**: SQLite database for historical performance data
- **HTML Reports**: Beautiful, responsive HTML comparison pages - **HTML Reports**: Beautiful, responsive HTML comparison pages
@ -38,6 +38,8 @@ pip install -r requirements.txt
cargo install cargo-aoc cargo install cargo-aoc
``` ```
Note: The script runs `cargo aoc` (not `cargo aoc bench`) and parses runtime information from the output.
## Configuration ## Configuration
Edit `config.yaml` to configure repositories to monitor: Edit `config.yaml` to configure repositories to monitor:
@ -118,8 +120,8 @@ The HTML report includes:
1. **Git Polling**: Checks each configured repository for changes by comparing local and remote commits 1. **Git Polling**: Checks each configured repository for changes by comparing local and remote commits
2. **Repository Update**: Clones new repositories or updates existing ones when changes are detected 2. **Repository Update**: Clones new repositories or updates existing ones when changes are detected
3. **Day Detection**: Automatically finds implemented days by scanning for `day*.rs` files and Cargo.toml entries 3. **Day Detection**: Automatically finds implemented days by scanning for `day*.rs` files and Cargo.toml entries
4. **Benchmarking**: Runs `cargo aoc bench --day X` for each implemented day 4. **Runtime Measurement**: Runs `cargo aoc --day X` for each implemented day
5. **Parsing**: Extracts timing data from cargo-aoc output (handles nanoseconds, microseconds, milliseconds) 5. **Parsing**: Extracts timing data from cargo-aoc output (handles nanoseconds, microseconds, milliseconds, seconds)
6. **Storage**: Stores results in SQLite database with timestamps 6. **Storage**: Stores results in SQLite database with timestamps
7. **Report Generation**: Generates HTML comparison page showing latest results 7. **Report Generation**: Generates HTML comparison page showing latest results
@ -146,8 +148,9 @@ If benchmarks take too long, the script has a 5-minute timeout per day. Adjust i
### Missing performance data ### Missing performance data
If some users/days/parts don't show up: If some users/days/parts don't show up:
- Check that `cargo aoc bench` runs successfully in the repository - Check that `cargo aoc --day X` runs successfully in the repository
- Verify the repository structure matches cargo-aoc conventions - Verify the repository structure matches cargo-aoc conventions
- Ensure `cargo aoc` outputs timing information (check if it's configured to show runtime)
- Check logs for parsing errors - Check logs for parsing errors
## License ## License

View File

@ -305,19 +305,25 @@ class CargoAOCRunner:
"""Runs cargo-aoc benchmarks and parses results""" """Runs cargo-aoc benchmarks and parses results"""
@staticmethod @staticmethod
def find_implemented_days(repo_path: Path) -> List[int]: def find_implemented_days(work_dir: Path) -> List[int]:
"""Find which days are implemented in the repository""" """Find which days are implemented in the directory
Args:
work_dir: Directory to search (should be a year directory for single repos)
"""
days = [] days = []
work_dir = Path(work_dir)
# Look for common patterns: src/bin/day01.rs, src/day01.rs, etc. # Look for common patterns: src/bin/day01.rs, src/day01.rs, etc.
patterns = [ patterns = [
repo_path / 'src' / 'bin' / 'day*.rs', 'src/bin/day*.rs',
repo_path / 'src' / 'day*.rs', 'src/day*.rs',
repo_path / 'src' / '**' / 'day*.rs', '**/src/bin/day*.rs',
'**/src/day*.rs',
] ]
for pattern in patterns: for pattern in patterns:
for day_file in repo_path.glob(str(pattern.relative_to(repo_path))): for day_file in work_dir.glob(pattern):
match = re.search(r'day(\d+)', day_file.name) match = re.search(r'day(\d+)', day_file.name)
if match: if match:
day_num = int(match.group(1)) day_num = int(match.group(1))
@ -325,7 +331,7 @@ class CargoAOCRunner:
days.append(day_num) days.append(day_num)
# Also check for Cargo.toml with day references # Also check for Cargo.toml with day references
cargo_toml = repo_path / 'Cargo.toml' cargo_toml = work_dir / 'Cargo.toml'
if cargo_toml.exists(): if cargo_toml.exists():
with open(cargo_toml, 'r') as f: with open(cargo_toml, 'r') as f:
content = f.read() content = f.read()
@ -338,140 +344,169 @@ class CargoAOCRunner:
@staticmethod @staticmethod
def extract_years_from_repo(repo_path: Path) -> List[int]: def extract_years_from_repo(repo_path: Path) -> List[int]:
"""Try to extract year(s) from repository path, name, or structure""" """Try to extract year(s) from repository structure
For single repos, looks for year directories in the root (e.g., 2023/, 2024/)
"""
years = [] years = []
repo_path = Path(repo_path) repo_path = Path(repo_path)
# Check path name # Check for year directories in root (e.g., 2023/, 2024/)
path_str = str(repo_path) # These should be directories with 4-digit year names
for year_match in re.finditer(r'(\d{4})', path_str):
year = int(year_match.group(1))
if 2015 <= year <= 2030 and year not in years: # Reasonable range
years.append(year)
# Check for year directories (common pattern: src/2023/, year2023/, etc.)
for item in repo_path.iterdir(): for item in repo_path.iterdir():
if item.is_dir(): if item.is_dir() and not item.name.startswith('.'):
year_match = re.search(r'(\d{4})', item.name) # Check if directory name is exactly a 4-digit year
if year_match: if re.match(r'^\d{4}$', item.name):
year = int(year_match.group(1)) year = int(item.name)
if 2015 <= year <= 2030 and year not in years: if 2015 <= year <= 2030: # Reasonable range
years.append(year) years.append(year)
# Check Cargo.toml # Also check path name as fallback
cargo_toml = repo_path / 'Cargo.toml' if not years:
if cargo_toml.exists(): path_str = str(repo_path)
with open(cargo_toml, 'r') as f: for year_match in re.finditer(r'(\d{4})', path_str):
content = f.read() year = int(year_match.group(1))
for year_match in re.finditer(r'(\d{4})', content): if 2015 <= year <= 2030 and year not in years:
year = int(year_match.group(1)) years.append(year)
if 2015 <= year <= 2030 and year not in years:
years.append(year)
return sorted(years) if years else [] return sorted(years) if years else []
@staticmethod @staticmethod
def run_benchmarks(repo_path: Path, year: Optional[int] = None, def run_benchmarks(repo_path: Path, year: int, user: str = "unknown",
user: str = "unknown") -> List[PerformanceResult]: is_multi_year: bool = False) -> List[PerformanceResult]:
"""Run cargo aoc benchmarks and parse results""" """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
is_multi_year: True if this is a multi-year repo (repo_path is already the year directory)
"""
results = [] results = []
repo_path = Path(repo_path) repo_path = Path(repo_path)
if not (repo_path / 'Cargo.toml').exists(): # Determine the working directory
logger.warning(f"No Cargo.toml found in {repo_path}") if is_multi_year:
# For multi-year repos, repo_path is already the year directory
work_dir = repo_path
else:
# For single repos, check if we need to navigate to a year subdirectory
work_dir = repo_path
year_dir = repo_path / str(year)
if year_dir.exists() and year_dir.is_dir():
work_dir = year_dir
logger.info(f"Using year directory: {work_dir}")
if not (work_dir / 'Cargo.toml').exists():
logger.warning(f"No Cargo.toml found in {work_dir}")
return results return results
days = CargoAOCRunner.find_implemented_days(repo_path) days = CargoAOCRunner.find_implemented_days(work_dir)
logger.info(f"Found {len(days)} implemented days in {repo_path}") logger.info(f"Found {len(days)} implemented days in {work_dir}")
for day in days: for day in days:
try: try:
# Run cargo aoc bench for this day logger.info(f"Running cargo aoc for {user} year {year} day {day} in {work_dir}")
cmd = ['cargo', 'aoc', 'bench', '--day', str(day)] # Run cargo aoc for this day (no year flag, must be in correct directory)
if year: cmd = ['cargo', 'aoc', '--day', str(day)]
cmd.extend(['--year', str(year)])
result = subprocess.run( result = subprocess.run(
cmd, cmd,
cwd=repo_path, cwd=work_dir,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=300 # 5 minute timeout per day timeout=300 # 5 minute timeout per day
) )
if result.returncode != 0: if result.returncode != 0:
logger.warning(f"cargo aoc bench failed for day {day}: {result.stderr}") logger.warning(f"cargo aoc failed for day {day} in {work_dir}: {result.stderr}")
continue continue
# Parse output for performance data # Parse output for runtime information
# Try to extract year from output if not provided day_results = CargoAOCRunner._parse_runtime_output(
actual_year = year result.stdout, result.stderr, day, year, user
if not actual_year:
# Look for year in output
year_match = re.search(r'(\d{4})', result.stdout)
if year_match:
potential_year = int(year_match.group(1))
if 2015 <= potential_year <= 2030:
actual_year = potential_year
if not actual_year:
logger.warning(f"Could not determine year for day {day}, skipping")
continue
day_results = CargoAOCRunner._parse_benchmark_output(
result.stdout, day, actual_year, user
) )
results.extend(day_results) results.extend(day_results)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error(f"Timeout running benchmarks for day {day}") logger.error(f"Timeout running cargo aoc for day {day}")
except Exception as e: except Exception as e:
logger.error(f"Error running benchmarks for day {day}: {e}") logger.error(f"Error running cargo aoc for day {day}: {e}")
return results return results
@staticmethod @staticmethod
def _parse_benchmark_output(output: str, day: int, year: int, def _parse_runtime_output(stdout: str, stderr: str, day: int, year: int,
user: str) -> List[PerformanceResult]: user: str) -> List[PerformanceResult]:
"""Parse cargo-aoc benchmark output""" """Parse cargo-aoc runtime output
cargo aoc typically outputs timing information like:
- "Day X - Part Y: XXX.XXX ms"
- "Day X - Part Y: XXX.XXX μs"
- "Day X - Part Y: XXX.XXX ns"
- Or similar formats
"""
results = [] results = []
timestamp = datetime.now().isoformat() timestamp = datetime.now().isoformat()
# Pattern: "Day X - Part Y: XXX.XXX ns (XXX.XXX ms)" # Combine stdout and stderr (timing info might be in either)
# or "Day X - Part Y: XXX.XXX ns" output = stdout + "\n" + stderr
# Also handles formats like "Day 1 Part 1", "day01-part1", etc.
# Patterns to match various cargo-aoc output formats
# Common formats:
# "Day 1 - Part 1: 123.456 ms"
# "Day 1 Part 1: 123.456 ms"
# "day 1 - part 1: 123.456 ms"
# "Part 1: 123.456 ms" (when day is already known)
patterns = [ patterns = [
r'Day\s+(\d+)\s*-\s*Part\s+(\d+)[:\s]+([\d.]+)\s*ns', # Full format with day and part
r'day\s+(\d+)\s*-\s*part\s+(\d+)[:\s]+([\d.]+)\s*ns', r'Day\s+(\d+)\s*-\s*Part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
r'Day\s+(\d+)\s+Part\s+(\d+)[:\s]+([\d.]+)\s*ns', r'day\s+(\d+)\s*-\s*part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
r'day\s+(\d+)\s+part\s+(\d+)[:\s]+([\d.]+)\s*ns', r'Day\s+(\d+)\s+Part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
r'day(\d+)\s*-\s*part(\d+)[:\s]+([\d.]+)\s*ns', r'day\s+(\d+)\s+part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
r'(\d+)\s*-\s*(\d+)[:\s]+([\d.]+)\s*ns', r'day(\d+)\s*-\s*part(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
# Handle microseconds and milliseconds too # Part only (use provided day)
r'Day\s+(\d+)\s*-\s*Part\s+(\d+)[:\s]+([\d.]+)\s*(?:ns|μs|us|ms)', r'Part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
r'part\s+(\d+)[:\s]+([\d.]+)\s*(ns|μs|us|ms|s)',
] ]
for pattern in patterns: for pattern in patterns:
for match in re.finditer(pattern, output, re.IGNORECASE): for match in re.finditer(pattern, output, re.IGNORECASE):
part_day = int(match.group(1)) groups = match.groups()
part_num = int(match.group(2))
time_str = match.group(3)
# Use the day from the match if available, otherwise use provided day # Determine day and part based on pattern
actual_day = part_day if part_day > 0 else day if len(groups) == 4:
# Pattern with day and part
part_day = int(groups[0])
part_num = int(groups[1])
time_str = groups[2]
unit = groups[3].lower()
actual_day = part_day if part_day > 0 and part_day <= 25 else day
elif len(groups) == 3:
# Pattern with only part (use provided day)
part_num = int(groups[0])
time_str = groups[1]
unit = groups[2].lower()
actual_day = day
else:
continue
try: try:
time_ns = int(float(time_str)) time_val = float(time_str)
# Check if the time unit is in the match (for patterns that include it) # Convert to nanoseconds
# If not, assume nanoseconds (most common) if unit == 's' or unit == 'sec' or unit == 'second':
# Look for unit in the original match context time_ns = int(time_val * 1_000_000_000)
match_text = match.group(0).lower() elif unit == 'ms' or unit == 'millisecond':
if 'ms' in match_text or 'millisecond' in match_text: time_ns = int(time_val * 1_000_000)
time_ns = int(time_ns * 1_000_000) # Convert ms to ns elif unit == 'μs' or unit == 'us' or unit == 'microsecond':
elif 'μs' in match_text or 'us' in match_text or 'microsecond' in match_text: time_ns = int(time_val * 1_000)
time_ns = int(time_ns * 1_000) # Convert μs to ns elif unit == 'ns' or unit == 'nanosecond':
# else: already in nanoseconds time_ns = int(time_val)
else:
# Default to nanoseconds if unit unclear
logger.warning(f"Unknown time unit '{unit}', assuming nanoseconds")
time_ns = int(time_val)
results.append(PerformanceResult( results.append(PerformanceResult(
user=user, user=user,
@ -894,7 +929,8 @@ class AOCSync:
if config_years: if config_years:
# Use years from config # Use years from config
for year in config_years: for year in config_years:
self._run_and_store_benchmarks(repo_path, year, user_name) self._run_and_store_benchmarks(repo_path, year, user_name,
is_multi_year=False)
else: else:
# Try to determine year(s) from the repository # Try to determine year(s) from the repository
years = CargoAOCRunner.extract_years_from_repo(repo_path) years = CargoAOCRunner.extract_years_from_repo(repo_path)
@ -902,19 +938,18 @@ class AOCSync:
if years: if years:
# Run benchmarks for each detected year # Run benchmarks for each detected year
for year in years: for year in years:
self._run_and_store_benchmarks(repo_path, year, user_name) self._run_and_store_benchmarks(repo_path, year, user_name,
is_multi_year=False)
else: else:
# If no year detected, try running without year specification # If no year detected, check for year directories
# cargo-aoc might infer it, or we'll try common years logger.warning(f"No year detected for {user_name}, checking for year directories")
logger.info(f"No year detected for {user_name}, trying without year specification") # Try common years as fallback
results = CargoAOCRunner.run_benchmarks(repo_path, year=None, user=user_name) for try_year in [2025, 2024, 2023, 2022, 2021, 2020]:
for result in results: year_dir = repo_path / str(try_year)
self.db.insert_result(result) if year_dir.exists() and year_dir.is_dir():
logger.info(f"Found year directory {try_year} for {user_name}")
# Also try common years as fallback self._run_and_store_benchmarks(repo_path, try_year, user_name,
for try_year in [2023, 2024, 2022, 2021, 2020]: is_multi_year=False)
if self._check_year_in_repo(repo_path, try_year):
self._run_and_store_benchmarks(repo_path, try_year, user_name)
elif repo_type == 'multi-year': elif repo_type == 'multi-year':
# Multiple repositories, one per year # Multiple repositories, one per year
@ -928,7 +963,8 @@ class AOCSync:
logger.info(f"Repository {user_name} year {year} has changes, updating...") logger.info(f"Repository {user_name} year {year} has changes, updating...")
if self.git_manager.clone_or_update_repo(url, local_path): if self.git_manager.clone_or_update_repo(url, local_path):
repo_path = Path(local_path) repo_path = Path(local_path)
self._run_and_store_benchmarks(repo_path, year, user_name) self._run_and_store_benchmarks(repo_path, year, user_name,
is_multi_year=True)
def _check_year_in_repo(self, repo_path: Path, year: int) -> bool: def _check_year_in_repo(self, repo_path: Path, year: int) -> bool:
"""Check if a repository contains solutions for a specific year""" """Check if a repository contains solutions for a specific year"""
@ -946,10 +982,12 @@ class AOCSync:
return False return False
def _run_and_store_benchmarks(self, repo_path: Path, year: int, user: str): def _run_and_store_benchmarks(self, repo_path: Path, year: int, user: str,
is_multi_year: bool = False):
"""Run benchmarks and store results""" """Run benchmarks and store results"""
logger.info(f"Running benchmarks for {user} year {year} in {repo_path}") logger.info(f"Running benchmarks for {user} year {year} in {repo_path}")
results = CargoAOCRunner.run_benchmarks(repo_path, year=year, user=user) results = CargoAOCRunner.run_benchmarks(repo_path, year=year, user=user,
is_multi_year=is_multi_year)
# Store results # Store results
for result in results: for result in results: