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
day: int
part: int
time_ns: int # Time in nanoseconds
timestamp: str
time_ns: int # Runner time in nanoseconds
generator_time_ns: int = 0 # Generator time in nanoseconds (optional)
timestamp: str = ""
class Config:
@ -105,11 +106,19 @@ class Database:
day INTEGER NOT NULL,
part INTEGER NOT NULL,
time_ns INTEGER NOT NULL,
generator_time_ns INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL,
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('''
CREATE INDEX IF NOT EXISTS idx_user_year_day_part
ON results(user, year, day, part)
@ -126,10 +135,10 @@ class Database:
try:
cursor.execute('''
INSERT OR REPLACE INTO results
(user, year, day, part, time_ns, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
(user, year, day, part, time_ns, generator_time_ns, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (result.user, result.year, result.day, result.part,
result.time_ns, result.timestamp))
result.time_ns, result.generator_time_ns, result.timestamp))
conn.commit()
except sqlite3.IntegrityError:
# Already exists, skip
@ -139,12 +148,15 @@ class Database:
def get_latest_results(self, years: Optional[List[int]] = None,
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)
cursor = conn.cursor()
query = '''
SELECT user, year, day, part, time_ns, timestamp
SELECT user, year, day, part, time_ns, generator_time_ns, timestamp
FROM results r1
WHERE timestamp = (
SELECT MAX(timestamp)
@ -159,12 +171,12 @@ class Database:
conditions = []
params = []
if years:
if years is not None:
placeholders = ','.join('?' * len(years))
conditions.append(f'year IN ({placeholders})')
params.extend(years)
if days:
if days is not None:
placeholders = ','.join('?' * len(days))
conditions.append(f'day IN ({placeholders})')
params.extend(days)
@ -185,7 +197,8 @@ class Database:
'day': row[2],
'part': row[3],
'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
]
@ -503,48 +516,71 @@ class CargoAOCRunner:
]
# 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')
current_day = 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):
# 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)
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_part = int(day_part_match.group(2))
actual_day = current_day if current_day > 0 and current_day <= 25 else day
generator_time_ns = 0
runner_time_ns = 0
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:
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)
if runner_match:
time_str = runner_match.group(1)
unit = runner_match.group(2).lower()
try:
time_val = float(time_str)
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
runner_time_ns = CargoAOCRunner._convert_to_nanoseconds(time_val, unit)
except ValueError:
logger.warning(f"Could not parse runner time: {time_str}")
# Check if next line starts a new Day/Part block (reset current context)
if i + 1 < len(lines):
next_day_match = re.match(r'Day\s+\d+\s*-\s*Part\s+\d+', lines[i + 1], re.IGNORECASE)
if next_day_match:
# Don't reset yet - let the next iteration handle it
pass
# Save the last part's data
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
))
# If we found results with the line-by-line approach, return them
if results:
@ -666,8 +702,6 @@ class CargoAOCRunner:
else:
# Default to nanoseconds
return int(time_val)
return results
class HTMLGenerator:
@ -679,10 +713,25 @@ class HTMLGenerator:
def generate(self, db: Database, config: Config):
"""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
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()
# Organize data by year -> day -> part -> user
@ -693,11 +742,18 @@ class HTMLGenerator:
day = result['day']
part = result['part']
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)
if time_ns > 0:
data[year][day][part][user] = time_ns
# Only store if runner_time_ns > 0 (valid result)
# Store total time (generator + runner) for comparison
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)
@ -709,6 +765,18 @@ class HTMLGenerator:
def _generate_html(self, data: dict, years: List[int], users: List[str]) -> str:
"""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 lang="en">
<head>
@ -726,91 +794,131 @@ class HTMLGenerator:
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
padding: 10px;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 30px;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 15px;
}}
h1 {{
color: #333;
margin-bottom: 10px;
font-size: 2.5em;
margin-bottom: 5px;
font-size: 1.8em;
}}
.subtitle {{
color: #666;
margin-bottom: 30px;
font-size: 1.1em;
margin-bottom: 15px;
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 {{
margin-bottom: 30px;
padding: 20px;
margin-bottom: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
border-radius: 6px;
font-size: 0.85em;
}}
.year-section {{
margin-bottom: 40px;
margin-bottom: 25px;
}}
.year-header {{
font-size: 2em;
font-size: 1.4em;
color: #667eea;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 2px solid #667eea;
}}
.day-section {{
margin-bottom: 30px;
padding: 20px;
margin-bottom: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
border-radius: 6px;
}}
.day-header {{
font-size: 1.5em;
font-size: 1.1em;
color: #333;
margin-bottom: 15px;
margin-bottom: 8px;
font-weight: 600;
}}
.part-section {{
margin-bottom: 20px;
margin-bottom: 12px;
}}
.part-header {{
font-size: 1.2em;
font-size: 0.95em;
color: #555;
margin-bottom: 10px;
margin-bottom: 5px;
font-weight: 600;
}}
table {{
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
margin-bottom: 8px;
background: white;
border-radius: 8px;
border-radius: 4px;
overflow: hidden;
font-size: 0.85em;
}}
th {{
background: #667eea;
color: white;
padding: 12px;
padding: 6px 8px;
text-align: left;
font-weight: 600;
}}
td {{
padding: 12px;
padding: 6px 8px;
border-bottom: 1px solid #e0e0e0;
}}
@ -821,6 +929,7 @@ class HTMLGenerator:
.time {{
font-family: 'Courier New', monospace;
font-weight: bold;
font-size: 0.9em;
}}
.fastest {{
@ -838,48 +947,42 @@ class HTMLGenerator:
font-style: italic;
}}
.stats {{
display: inline-block;
margin-left: 10px;
font-size: 0.9em;
color: #666;
}}
.summary {{
margin-bottom: 30px;
padding: 20px;
margin-top: 30px;
padding: 15px;
background: #e3f2fd;
border-radius: 8px;
border-radius: 6px;
border-left: 4px solid #2196f3;
}}
.summary h3 {{
color: #1976d2;
margin-bottom: 15px;
margin-bottom: 10px;
font-size: 1.1em;
}}
.summary-stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 10px;
}}
.stat-item {{
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 10px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}}
.stat-label {{
font-size: 0.9em;
font-size: 0.8em;
color: #666;
margin-bottom: 5px;
margin-bottom: 3px;
}}
.stat-value {{
font-size: 1.5em;
font-size: 1.3em;
font-weight: bold;
color: #333;
}}
@ -890,54 +993,30 @@ class HTMLGenerator:
<h1>🎄 Advent of Code Performance Comparison</h1>
<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">
<h3>Comparison Settings</h3>
<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>
<p><strong>Users:</strong> {', '.join(sorted(users))}</p>
</div>
"""
# Generate content for each year
for year in sorted(years):
# Generate content for each year (sorted descending)
for year in sorted_years:
if year not in data:
continue
html += f"""
<div class="year-section">
<div class="year-section" id="year-{year}">
<h2 class="year-header">Year {year}</h2>
"""
@ -955,14 +1034,23 @@ class HTMLGenerator:
if not part_data:
continue
# Find fastest and slowest
times = [(user, time_ns) for user, time_ns in part_data.items() if time_ns > 0]
# Find fastest and slowest (using total time)
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:
continue
times.sort(key=lambda x: x[1])
fastest_time = times[0][1]
slowest_time = times[-1][1]
times.sort(key=lambda x: x[2]) # Sort by total time
fastest_time = times[0][2]
slowest_time = times[-1][2]
html += f"""
<div class="part-section">
@ -971,56 +1059,100 @@ class HTMLGenerator:
<thead>
<tr>
<th>User</th>
<th>Time</th>
<th>Total Time</th>
<th>Generator</th>
<th>Runner</th>
<th>Relative Speed</th>
</tr>
</thead>
<tbody>
"""
# Sort users by time (include all users, even if no data)
user_times = [(user, part_data.get(user, 0)) for user in users]
sorted_users = sorted(user_times, key=lambda x: x[1] if x[1] > 0 else float('inf'))
# Sort users by total time (include all users, even if no data)
user_times = []
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:
if time_ns == 0:
sorted_users = sorted(user_times, key=lambda x: x[2] if x[2] > 0 else float('inf'))
for user, time_data, total_time_ns in sorted_users:
if total_time_ns == 0:
html += f"""
<tr>
<td>{user}</td>
<td class="no-data">No data</td>
<td class="no-data">-</td>
<td class="no-data">-</td>
<td class="no-data">-</td>
</tr>
"""
else:
time_ms = time_ns / 1_000_000
time_us = time_ns / 1_000
# Format time appropriately
if time_ms >= 1:
time_str = f"{time_ms:.2f} ms"
elif time_us >= 1:
time_str = f"{time_us:.2f} μs"
# Extract times
if isinstance(time_data, dict):
runner_time_ns = time_data.get('runner', 0)
generator_time_ns = time_data.get('generator', 0)
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:
relative = time_ns / fastest_time
relative = total_time_ns / fastest_time
relative_str = f"{relative:.2f}x"
else:
relative_str = "-"
# Determine if fastest or slowest
row_class = ""
if time_ns == fastest_time:
if total_time_ns == fastest_time:
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"
html += f"""
<tr class="{row_class}">
<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>
</tr>
"""
@ -1039,6 +1171,31 @@ class HTMLGenerator:
</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 += """
</div>
</body>