Include generator time in the timings
This commit is contained in:
parent
d8b0bb7acc
commit
ac85c2defa
459
aocsync.py
459
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"""<!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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user