Include generator time in the timings

This commit is contained in:
Bill Thiede 2025-12-03 09:13:50 -08:00
parent d8b0bb7acc
commit ac85c2defa

View File

@ -35,8 +35,9 @@ class PerformanceResult:
year: int year: int
day: int day: int
part: int part: int
time_ns: int # Time in nanoseconds time_ns: int # Runner time in nanoseconds
timestamp: str generator_time_ns: int = 0 # Generator time in nanoseconds (optional)
timestamp: str = ""
class Config: class Config:
@ -105,11 +106,19 @@ class Database:
day INTEGER NOT NULL, day INTEGER NOT NULL,
part INTEGER NOT NULL, part INTEGER NOT NULL,
time_ns INTEGER NOT NULL, time_ns INTEGER NOT NULL,
generator_time_ns INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL, timestamp TEXT NOT NULL,
UNIQUE(user, year, day, part, timestamp) UNIQUE(user, year, day, part, timestamp)
) )
''') ''')
# 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
cursor.execute(''' cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_user_year_day_part CREATE INDEX IF NOT EXISTS idx_user_year_day_part
ON results(user, year, day, part) ON results(user, year, day, part)
@ -126,10 +135,10 @@ class Database:
try: try:
cursor.execute(''' cursor.execute('''
INSERT OR REPLACE INTO results INSERT OR REPLACE INTO results
(user, year, day, part, time_ns, timestamp) (user, year, day, part, time_ns, generator_time_ns, timestamp)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (result.user, result.year, result.day, result.part, ''', (result.user, result.year, result.day, result.part,
result.time_ns, result.timestamp)) result.time_ns, result.generator_time_ns, result.timestamp))
conn.commit() conn.commit()
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
# Already exists, skip # Already exists, skip
@ -139,12 +148,15 @@ class Database:
def get_latest_results(self, years: Optional[List[int]] = None, def get_latest_results(self, years: Optional[List[int]] = None,
days: Optional[List[int]] = None) -> List[Dict]: days: Optional[List[int]] = None) -> List[Dict]:
"""Get latest performance results for each user/day/part""" """Get latest performance results for each user/day/part
If years is None, returns all years. If days is None, returns all days.
"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
query = ''' query = '''
SELECT user, year, day, part, time_ns, timestamp SELECT user, year, day, part, time_ns, generator_time_ns, timestamp
FROM results r1 FROM results r1
WHERE timestamp = ( WHERE timestamp = (
SELECT MAX(timestamp) SELECT MAX(timestamp)
@ -159,12 +171,12 @@ class Database:
conditions = [] conditions = []
params = [] params = []
if years: if years is not None:
placeholders = ','.join('?' * len(years)) placeholders = ','.join('?' * len(years))
conditions.append(f'year IN ({placeholders})') conditions.append(f'year IN ({placeholders})')
params.extend(years) params.extend(years)
if days: if days is not None:
placeholders = ','.join('?' * len(days)) placeholders = ','.join('?' * len(days))
conditions.append(f'day IN ({placeholders})') conditions.append(f'day IN ({placeholders})')
params.extend(days) params.extend(days)
@ -185,7 +197,8 @@ class Database:
'day': row[2], 'day': row[2],
'part': row[3], 'part': row[3],
'time_ns': row[4], 'time_ns': row[4],
'timestamp': row[5] 'generator_time_ns': row[5] if len(row) > 5 else 0,
'timestamp': row[6] if len(row) > 6 else row[5]
} }
for row in rows for row in rows
] ]
@ -503,48 +516,71 @@ class CargoAOCRunner:
] ]
# First, try to parse the generator/runner format which is most common # First, try to parse the generator/runner format which is most common
# Look for "Day X - Part Y" lines and extract runner times from following lines # Look for "Day X - Part Y" lines and extract both generator and runner times
lines = output.split('\n') lines = output.split('\n')
current_day = None current_day = None
current_part = None current_part = None
actual_day = day # Default to provided day
generator_time_ns = 0
runner_time_ns = 0
for i, line in enumerate(lines): for i, line in enumerate(lines):
# Check if this line starts a new Day/Part block # Check if this line starts a new Day/Part block
day_part_match = re.match(r'Day\s+(\d+)\s*-\s*Part\s+(\d+)[:\s]', line, re.IGNORECASE) day_part_match = re.match(r'Day\s+(\d+)\s*-\s*Part\s+(\d+)[:\s]', line, re.IGNORECASE)
if day_part_match: if day_part_match:
# Save previous part's data if we have it
if current_day is not None and current_part is not None and runner_time_ns > 0:
results.append(PerformanceResult(
user=user,
year=year,
day=actual_day,
part=current_part,
time_ns=runner_time_ns,
generator_time_ns=generator_time_ns,
timestamp=timestamp
))
# Start new part
current_day = int(day_part_match.group(1)) current_day = int(day_part_match.group(1))
current_part = int(day_part_match.group(2)) current_part = int(day_part_match.group(2))
actual_day = current_day if current_day > 0 and current_day <= 25 else day actual_day = current_day if current_day > 0 and current_day <= 25 else day
generator_time_ns = 0
runner_time_ns = 0
continue continue
# If we're in a Day/Part block, look for runner timing # If we're in a Day/Part block, look for generator and runner timing
if current_day is not None and current_part is not None: if current_day is not None and current_part is not None:
generator_match = re.search(r'generator\s*:\s*([\d.]+)\s*(ns|μs|µs|us|ms|s|sec)', line, re.IGNORECASE)
if generator_match:
time_str = generator_match.group(1)
unit = generator_match.group(2).lower()
try:
time_val = float(time_str)
generator_time_ns = CargoAOCRunner._convert_to_nanoseconds(time_val, unit)
except ValueError:
logger.warning(f"Could not parse generator time: {time_str}")
runner_match = re.search(r'runner\s*:\s*([\d.]+)\s*(ns|μs|µs|us|ms|s|sec)', line, re.IGNORECASE) runner_match = re.search(r'runner\s*:\s*([\d.]+)\s*(ns|μs|µs|us|ms|s|sec)', line, re.IGNORECASE)
if runner_match: if runner_match:
time_str = runner_match.group(1) time_str = runner_match.group(1)
unit = runner_match.group(2).lower() unit = runner_match.group(2).lower()
try: try:
time_val = float(time_str) time_val = float(time_str)
time_ns = CargoAOCRunner._convert_to_nanoseconds(time_val, unit) runner_time_ns = CargoAOCRunner._convert_to_nanoseconds(time_val, unit)
results.append(PerformanceResult(
user=user,
year=year,
day=actual_day,
part=current_part,
time_ns=time_ns,
timestamp=timestamp
))
# Reset after finding runner (in case there are multiple parts)
# But keep current_day/current_part until we hit the next Day line
except ValueError: except ValueError:
logger.warning(f"Could not parse runner time: {time_str}") logger.warning(f"Could not parse runner time: {time_str}")
# Check if next line starts a new Day/Part block (reset current context) # Save the last part's data
if i + 1 < len(lines): if current_day is not None and current_part is not None and runner_time_ns > 0:
next_day_match = re.match(r'Day\s+\d+\s*-\s*Part\s+\d+', lines[i + 1], re.IGNORECASE) results.append(PerformanceResult(
if next_day_match: user=user,
# Don't reset yet - let the next iteration handle it year=year,
pass day=actual_day,
part=current_part,
time_ns=runner_time_ns,
generator_time_ns=generator_time_ns,
timestamp=timestamp
))
# If we found results with the line-by-line approach, return them # If we found results with the line-by-line approach, return them
if results: if results:
@ -667,8 +703,6 @@ class CargoAOCRunner:
# Default to nanoseconds # Default to nanoseconds
return int(time_val) return int(time_val)
return results
class HTMLGenerator: class HTMLGenerator:
"""Generates HTML comparison pages""" """Generates HTML comparison pages"""
@ -679,10 +713,25 @@ class HTMLGenerator:
def generate(self, db: Database, config: Config): def generate(self, db: Database, config: Config):
"""Generate HTML comparison page""" """Generate HTML comparison page"""
years = config.compare_years or db.get_all_years() # Get all years from database, but filter by compare_years if specified
all_years_in_db = db.get_all_years()
if config.compare_years:
# Only include years that are both in compare_years AND in the database
years = [y for y in config.compare_years if y in all_years_in_db]
if not years:
logger.warning(f"compare_years {config.compare_years} specified but no matching data found. Using all years from database.")
years = all_years_in_db
else:
# Use all years from database
years = all_years_in_db
days = config.compare_days days = config.compare_days
results = db.get_latest_results(years=years, days=days) results = db.get_latest_results(years=None, days=days) # Get all years, filter in Python
# Filter results by years if needed
if years:
results = [r for r in results if r['year'] in years]
users = db.get_all_users() users = db.get_all_users()
# Organize data by year -> day -> part -> user # Organize data by year -> day -> part -> user
@ -693,11 +742,18 @@ class HTMLGenerator:
day = result['day'] day = result['day']
part = result['part'] part = result['part']
user = result['user'] user = result['user']
time_ns = result['time_ns'] runner_time_ns = result['time_ns']
generator_time_ns = result.get('generator_time_ns', 0)
# Only store if time_ns > 0 (valid result) # Only store if runner_time_ns > 0 (valid result)
if time_ns > 0: # Store total time (generator + runner) for comparison
data[year][day][part][user] = time_ns if runner_time_ns > 0:
total_time_ns = runner_time_ns + generator_time_ns
data[year][day][part][user] = {
'total': total_time_ns,
'runner': runner_time_ns,
'generator': generator_time_ns
}
html = self._generate_html(data, years, users) html = self._generate_html(data, years, users)
@ -709,6 +765,18 @@ class HTMLGenerator:
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]) -> str:
"""Generate HTML content""" """Generate HTML content"""
# Sort years descending (most recent first)
sorted_years = sorted(years, reverse=True)
# Calculate summary statistics
total_days = sum(len(data[year]) for year in data)
total_parts = sum(len(parts) for year in data for day in data[year].values() for parts in day.values())
users_with_data = set()
for year in data.values():
for day in year.values():
for part in day.values():
users_with_data.update(part.keys())
html = f"""<!DOCTYPE html> html = f"""<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -726,91 +794,131 @@ class HTMLGenerator:
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
padding: 20px; padding: 10px;
}} }}
.container {{ .container {{
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
background: white; background: white;
border-radius: 12px; border-radius: 8px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 30px; padding: 15px;
}} }}
h1 {{ h1 {{
color: #333; color: #333;
margin-bottom: 10px; margin-bottom: 5px;
font-size: 2.5em; font-size: 1.8em;
}} }}
.subtitle {{ .subtitle {{
color: #666; color: #666;
margin-bottom: 30px; margin-bottom: 15px;
font-size: 1.1em; font-size: 0.9em;
}}
.nav-bar {{
margin-bottom: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
position: sticky;
top: 10px;
z-index: 100;
}}
.nav-bar h3 {{
font-size: 0.9em;
color: #555;
margin-bottom: 8px;
}}
.nav-links {{
display: flex;
flex-wrap: wrap;
gap: 8px;
}}
.nav-link {{
padding: 4px 12px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 0.85em;
transition: background 0.2s;
}}
.nav-link:hover {{
background: #5568d3;
}} }}
.controls {{ .controls {{
margin-bottom: 30px; margin-bottom: 15px;
padding: 20px; padding: 10px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 8px; border-radius: 6px;
font-size: 0.85em;
}} }}
.year-section {{ .year-section {{
margin-bottom: 40px; margin-bottom: 25px;
}} }}
.year-header {{ .year-header {{
font-size: 2em; font-size: 1.4em;
color: #667eea; color: #667eea;
margin-bottom: 20px; margin-bottom: 10px;
padding-bottom: 10px; padding-bottom: 5px;
border-bottom: 3px solid #667eea; border-bottom: 2px solid #667eea;
}} }}
.day-section {{ .day-section {{
margin-bottom: 30px; margin-bottom: 15px;
padding: 20px; padding: 10px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 8px; border-radius: 6px;
}} }}
.day-header {{ .day-header {{
font-size: 1.5em; font-size: 1.1em;
color: #333; color: #333;
margin-bottom: 15px; margin-bottom: 8px;
font-weight: 600;
}} }}
.part-section {{ .part-section {{
margin-bottom: 20px; margin-bottom: 12px;
}} }}
.part-header {{ .part-header {{
font-size: 1.2em; font-size: 0.95em;
color: #555; color: #555;
margin-bottom: 10px; margin-bottom: 5px;
font-weight: 600;
}} }}
table {{ table {{
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 15px; margin-bottom: 8px;
background: white; background: white;
border-radius: 8px; border-radius: 4px;
overflow: hidden; overflow: hidden;
font-size: 0.85em;
}} }}
th {{ th {{
background: #667eea; background: #667eea;
color: white; color: white;
padding: 12px; padding: 6px 8px;
text-align: left; text-align: left;
font-weight: 600; font-weight: 600;
}} }}
td {{ td {{
padding: 12px; padding: 6px 8px;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
}} }}
@ -821,6 +929,7 @@ class HTMLGenerator:
.time {{ .time {{
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-weight: bold; font-weight: bold;
font-size: 0.9em;
}} }}
.fastest {{ .fastest {{
@ -838,48 +947,42 @@ class HTMLGenerator:
font-style: italic; font-style: italic;
}} }}
.stats {{
display: inline-block;
margin-left: 10px;
font-size: 0.9em;
color: #666;
}}
.summary {{ .summary {{
margin-bottom: 30px; margin-top: 30px;
padding: 20px; padding: 15px;
background: #e3f2fd; background: #e3f2fd;
border-radius: 8px; border-radius: 6px;
border-left: 4px solid #2196f3; border-left: 4px solid #2196f3;
}} }}
.summary h3 {{ .summary h3 {{
color: #1976d2; color: #1976d2;
margin-bottom: 15px; margin-bottom: 10px;
font-size: 1.1em;
}} }}
.summary-stats {{ .summary-stats {{
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px; gap: 10px;
margin-top: 15px; margin-top: 10px;
}} }}
.stat-item {{ .stat-item {{
background: white; background: white;
padding: 15px; padding: 10px;
border-radius: 6px; border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}} }}
.stat-label {{ .stat-label {{
font-size: 0.9em; font-size: 0.8em;
color: #666; color: #666;
margin-bottom: 5px; margin-bottom: 3px;
}} }}
.stat-value {{ .stat-value {{
font-size: 1.5em; font-size: 1.3em;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
}} }}
@ -890,54 +993,30 @@ class HTMLGenerator:
<h1>🎄 Advent of Code Performance Comparison</h1> <h1>🎄 Advent of Code Performance Comparison</h1>
<p class="subtitle">Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p> <p class="subtitle">Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="nav-bar">
<h3>Jump to Year:</h3>
<div class="nav-links">
"""
# Add navigation links for each year
for year in sorted_years:
html += f' <a href="#year-{year}" class="nav-link">{year}</a>\n'
html += """ </div>
</div>
<div class="controls"> <div class="controls">
<h3>Comparison Settings</h3> <p><strong>Users:</strong> {', '.join(sorted(users))}</p>
<p>Years: {', '.join(map(str, sorted(years)))}</p>
<p>Users: {', '.join(sorted(users))}</p>
</div>
<div class="summary">
<h3>Summary Statistics</h3>
<div class="summary-stats">
"""
# Calculate summary statistics
total_days = sum(len(data[year]) for year in data)
total_parts = sum(len(parts) for year in data for day in data[year].values() for parts in day.values())
users_with_data = set()
for year in data.values():
for day in year.values():
for part in day.values():
users_with_data.update(part.keys())
html += f"""
<div class="stat-item">
<div class="stat-label">Total Years</div>
<div class="stat-value">{len(data)}</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Days</div>
<div class="stat-value">{total_days}</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Parts</div>
<div class="stat-value">{total_parts}</div>
</div>
<div class="stat-item">
<div class="stat-label">Users with Data</div>
<div class="stat-value">{len(users_with_data)}</div>
</div>
</div>
</div> </div>
""" """
# Generate content for each year # Generate content for each year (sorted descending)
for year in sorted(years): for year in sorted_years:
if year not in data: if year not in data:
continue continue
html += f""" html += f"""
<div class="year-section"> <div class="year-section" id="year-{year}">
<h2 class="year-header">Year {year}</h2> <h2 class="year-header">Year {year}</h2>
""" """
@ -955,14 +1034,23 @@ class HTMLGenerator:
if not part_data: if not part_data:
continue continue
# Find fastest and slowest # Find fastest and slowest (using total time)
times = [(user, time_ns) for user, time_ns in part_data.items() if time_ns > 0] times = []
for user, time_data in part_data.items():
if isinstance(time_data, dict):
total_time = time_data.get('total', 0)
else:
# Backward compatibility with old format
total_time = time_data if time_data > 0 else 0
if total_time > 0:
times.append((user, time_data, total_time))
if not times: if not times:
continue continue
times.sort(key=lambda x: x[1]) times.sort(key=lambda x: x[2]) # Sort by total time
fastest_time = times[0][1] fastest_time = times[0][2]
slowest_time = times[-1][1] slowest_time = times[-1][2]
html += f""" html += f"""
<div class="part-section"> <div class="part-section">
@ -971,56 +1059,100 @@ class HTMLGenerator:
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
<th>Time</th> <th>Total Time</th>
<th>Generator</th>
<th>Runner</th>
<th>Relative Speed</th> <th>Relative Speed</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
""" """
# Sort users by time (include all users, even if no data) # Sort users by total time (include all users, even if no data)
user_times = [(user, part_data.get(user, 0)) for user in users] user_times = []
sorted_users = sorted(user_times, key=lambda x: x[1] if x[1] > 0 else float('inf')) for user in users:
time_data = part_data.get(user, 0)
if isinstance(time_data, dict):
total_time = time_data.get('total', 0)
else:
total_time = time_data if time_data > 0 else 0
user_times.append((user, time_data, total_time))
for user, time_ns in sorted_users: sorted_users = sorted(user_times, key=lambda x: x[2] if x[2] > 0 else float('inf'))
if time_ns == 0:
for user, time_data, total_time_ns in sorted_users:
if total_time_ns == 0:
html += f""" html += f"""
<tr> <tr>
<td>{user}</td> <td>{user}</td>
<td class="no-data">No data</td> <td class="no-data">No data</td>
<td class="no-data">-</td> <td class="no-data">-</td>
<td class="no-data">-</td>
<td class="no-data">-</td>
</tr> </tr>
""" """
else: else:
time_ms = time_ns / 1_000_000 # Extract times
time_us = time_ns / 1_000 if isinstance(time_data, dict):
runner_time_ns = time_data.get('runner', 0)
# Format time appropriately generator_time_ns = time_data.get('generator', 0)
if time_ms >= 1:
time_str = f"{time_ms:.2f} ms"
elif time_us >= 1:
time_str = f"{time_us:.2f} μs"
else: else:
time_str = f"{time_ns} ns" # Backward compatibility
runner_time_ns = total_time_ns
generator_time_ns = 0
# Calculate relative speed # Format total time
total_ms = total_time_ns / 1_000_000
total_us = total_time_ns / 1_000
if total_ms >= 1:
total_str = f"{total_ms:.2f} ms"
elif total_us >= 1:
total_str = f"{total_us:.2f} μs"
else:
total_str = f"{total_time_ns} ns"
# Format generator time
gen_ms = generator_time_ns / 1_000_000
gen_us = generator_time_ns / 1_000
if gen_ms >= 1:
gen_str = f"{gen_ms:.2f} ms"
elif gen_us >= 1:
gen_str = f"{gen_us:.2f} μs"
elif generator_time_ns > 0:
gen_str = f"{generator_time_ns} ns"
else:
gen_str = "-"
# Format runner time
run_ms = runner_time_ns / 1_000_000
run_us = runner_time_ns / 1_000
if run_ms >= 1:
run_str = f"{run_ms:.2f} ms"
elif run_us >= 1:
run_str = f"{run_us:.2f} μs"
else:
run_str = f"{runner_time_ns} ns"
# Calculate relative speed (based on total time)
if fastest_time > 0: if fastest_time > 0:
relative = time_ns / fastest_time relative = total_time_ns / fastest_time
relative_str = f"{relative:.2f}x" relative_str = f"{relative:.2f}x"
else: else:
relative_str = "-" relative_str = "-"
# Determine if fastest or slowest # Determine if fastest or slowest
row_class = "" row_class = ""
if time_ns == fastest_time: if total_time_ns == fastest_time:
row_class = "fastest" row_class = "fastest"
elif time_ns == slowest_time and len(times) > 1: elif total_time_ns == slowest_time and len(times) > 1:
row_class = "slowest" row_class = "slowest"
html += f""" html += f"""
<tr class="{row_class}"> <tr class="{row_class}">
<td>{user}</td> <td>{user}</td>
<td class="time">{time_str}</td> <td class="time">{total_str}</td>
<td class="time">{gen_str}</td>
<td class="time">{run_str}</td>
<td>{relative_str}</td> <td>{relative_str}</td>
</tr> </tr>
""" """
@ -1039,6 +1171,31 @@ class HTMLGenerator:
</div> </div>
""" """
# Add summary statistics at the bottom
html += f"""
<div class="summary">
<h3>Summary Statistics</h3>
<div class="summary-stats">
<div class="stat-item">
<div class="stat-label">Total Years</div>
<div class="stat-value">{len(data)}</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Days</div>
<div class="stat-value">{total_days}</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Parts</div>
<div class="stat-value">{total_parts}</div>
</div>
<div class="stat-item">
<div class="stat-label">Users with Data</div>
<div class="stat-value">{len(users_with_data)}</div>
</div>
</div>
</div>
"""
html += """ html += """
</div> </div>
</body> </body>