From ac85c2defa8b808a928f33ebea5ab85342a2b1b5 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Wed, 3 Dec 2025 09:13:50 -0800 Subject: [PATCH] Include generator time in the timings --- aocsync.py | 459 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 308 insertions(+), 151 deletions(-) diff --git a/aocsync.py b/aocsync.py index bbe25c5..8477f5f 100755 --- a/aocsync.py +++ b/aocsync.py @@ -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""" @@ -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:

🎄 Advent of Code Performance Comparison

Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+ +
-

Comparison Settings

-

Years: {', '.join(map(str, sorted(years)))}

-

Users: {', '.join(sorted(users))}

-
- -
-

Summary Statistics

-
-""" - - # 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""" -
-
Total Years
-
{len(data)}
-
-
-
Total Days
-
{total_days}
-
-
-
Total Parts
-
{total_parts}
-
-
-
Users with Data
-
{len(users_with_data)}
-
-
+

Users: {', '.join(sorted(users))}

""" - # 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""" -
+

Year {year}

""" @@ -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"""
@@ -971,56 +1059,100 @@ class HTMLGenerator: User - Time + Total Time + Generator + Runner Relative Speed """ - # 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""" {user} No data - + - + - """ 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""" {user} - {time_str} + {total_str} + {gen_str} + {run_str} {relative_str} """ @@ -1039,6 +1171,31 @@ class HTMLGenerator:
""" + # Add summary statistics at the bottom + html += f""" +
+

Summary Statistics

+
+
+
Total Years
+
{len(data)}
+
+
+
Total Days
+
{total_days}
+
+
+
Total Parts
+
{total_parts}
+
+
+
Users with Data
+
{len(users_with_data)}
+
+
+
+""" + html += """