Show bytes output counters

This commit is contained in:
Bill Thiede 2025-12-04 17:23:44 -08:00
parent c851648166
commit 171fdb4329

View File

@ -37,6 +37,7 @@ class PerformanceResult:
part: int part: int
time_ns: int # Runner time in nanoseconds time_ns: int # Runner time in nanoseconds
generator_time_ns: int = 0 # Generator time in nanoseconds (optional) generator_time_ns: int = 0 # Generator time in nanoseconds (optional)
output_bytes: int = 0 # Number of bytes in the output/answer
git_rev: str = "" # Git revision (short hash) git_rev: str = "" # Git revision (short hash)
repo_url: str = "" # Repository URL repo_url: str = "" # Repository URL
timestamp: str = "" timestamp: str = ""
@ -121,6 +122,7 @@ class Database:
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, generator_time_ns INTEGER NOT NULL DEFAULT 0,
output_bytes INTEGER NOT NULL DEFAULT 0,
git_rev TEXT NOT NULL DEFAULT '', git_rev TEXT NOT NULL DEFAULT '',
repo_url TEXT NOT NULL DEFAULT '', repo_url TEXT NOT NULL DEFAULT '',
timestamp TEXT NOT NULL, timestamp TEXT NOT NULL,
@ -131,6 +133,7 @@ class Database:
# Add new columns if they don't exist (for existing databases) # Add new columns if they don't exist (for existing databases)
for column, col_type in [ for column, col_type in [
('generator_time_ns', 'INTEGER NOT NULL DEFAULT 0'), ('generator_time_ns', 'INTEGER NOT NULL DEFAULT 0'),
('output_bytes', 'INTEGER NOT NULL DEFAULT 0'),
('git_rev', 'TEXT NOT NULL DEFAULT \'\''), ('git_rev', 'TEXT NOT NULL DEFAULT \'\''),
('repo_url', 'TEXT NOT NULL DEFAULT \'\'') ('repo_url', 'TEXT NOT NULL DEFAULT \'\'')
]: ]:
@ -156,10 +159,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, generator_time_ns, git_rev, repo_url, timestamp) (user, year, day, part, time_ns, generator_time_ns, output_bytes, git_rev, repo_url, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (result.user, result.year, result.day, result.part, ''', (result.user, result.year, result.day, result.part,
result.time_ns, result.generator_time_ns, result.git_rev, result.time_ns, result.generator_time_ns, result.output_bytes, result.git_rev,
result.repo_url, result.timestamp)) result.repo_url, result.timestamp))
conn.commit() conn.commit()
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
@ -178,7 +181,7 @@ class Database:
cursor = conn.cursor() cursor = conn.cursor()
query = ''' query = '''
SELECT user, year, day, part, time_ns, generator_time_ns, git_rev, repo_url, timestamp SELECT user, year, day, part, time_ns, generator_time_ns, output_bytes, git_rev, repo_url, timestamp
FROM results r1 FROM results r1
WHERE timestamp = ( WHERE timestamp = (
SELECT MAX(timestamp) SELECT MAX(timestamp)
@ -220,9 +223,10 @@ class Database:
'part': row[3], 'part': row[3],
'time_ns': row[4], 'time_ns': row[4],
'generator_time_ns': row[5] if len(row) > 5 else 0, 'generator_time_ns': row[5] if len(row) > 5 else 0,
'git_rev': row[6] if len(row) > 6 else '', 'output_bytes': row[6] if len(row) > 6 else 0,
'repo_url': row[7] if len(row) > 7 else '', 'git_rev': row[7] if len(row) > 7 else '',
'timestamp': row[8] if len(row) > 8 else (row[6] if len(row) > 6 else '') 'repo_url': row[8] if len(row) > 8 else '',
'timestamp': row[9] if len(row) > 9 else (row[7] if len(row) > 7 else '')
} }
for row in rows for row in rows
] ]
@ -785,9 +789,12 @@ class CargoAOCRunner:
stdout_clean = CargoAOCRunner._strip_ansi_codes(result.stdout or "") stdout_clean = CargoAOCRunner._strip_ansi_codes(result.stdout or "")
stderr_clean = CargoAOCRunner._strip_ansi_codes(result.stderr or "") stderr_clean = CargoAOCRunner._strip_ansi_codes(result.stderr or "")
# Count bytes in stdout output (original, before ANSI stripping)
output_bytes = len(result.stdout.encode('utf-8')) if result.stdout else 0
# Parse output for runtime information # Parse output for runtime information
day_results = CargoAOCRunner._parse_runtime_output( day_results = CargoAOCRunner._parse_runtime_output(
stdout_clean, stderr_clean, day, year, user, git_rev, repo_url stdout_clean, stderr_clean, day, year, user, git_rev, repo_url, output_bytes
) )
if day_results: if day_results:
logger.info(f"Parsed {len(day_results)} runtime result(s) for {user} year {year} day {day}") logger.info(f"Parsed {len(day_results)} runtime result(s) for {user} year {year} day {day}")
@ -816,7 +823,7 @@ class CargoAOCRunner:
@staticmethod @staticmethod
def _parse_runtime_output(stdout: str, stderr: str, day: int, year: int, def _parse_runtime_output(stdout: str, stderr: str, day: int, year: int,
user: str, git_rev: str = "", repo_url: str = "") -> List[PerformanceResult]: user: str, git_rev: str = "", repo_url: str = "", output_bytes: int = 0) -> List[PerformanceResult]:
"""Parse cargo-aoc runtime output """Parse cargo-aoc runtime output
cargo aoc typically outputs timing information like: cargo aoc typically outputs timing information like:
@ -869,6 +876,7 @@ 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 both generator and runner times # Look for "Day X - Part Y" lines and extract both generator and runner times
# output_bytes parameter contains the total stdout bytes for this day run
lines = output.split('\n') lines = output.split('\n')
current_day = None current_day = None
current_part = None current_part = None
@ -889,6 +897,7 @@ class CargoAOCRunner:
part=current_part, part=current_part,
time_ns=runner_time_ns, time_ns=runner_time_ns,
generator_time_ns=generator_time_ns, generator_time_ns=generator_time_ns,
output_bytes=output_bytes,
git_rev=git_rev, git_rev=git_rev,
repo_url=repo_url, repo_url=repo_url,
timestamp=timestamp timestamp=timestamp
@ -933,6 +942,7 @@ class CargoAOCRunner:
part=current_part, part=current_part,
time_ns=runner_time_ns, time_ns=runner_time_ns,
generator_time_ns=generator_time_ns, generator_time_ns=generator_time_ns,
output_bytes=output_bytes,
git_rev=git_rev, git_rev=git_rev,
repo_url=repo_url, repo_url=repo_url,
timestamp=timestamp timestamp=timestamp
@ -994,6 +1004,7 @@ class CargoAOCRunner:
day=actual_day, day=actual_day,
part=part_num, part=part_num,
time_ns=time_ns, time_ns=time_ns,
output_bytes=output_bytes,
timestamp=timestamp timestamp=timestamp
)) ))
except ValueError: except ValueError:
@ -1023,6 +1034,7 @@ class CargoAOCRunner:
day=day, day=day,
part=1, part=1,
time_ns=time_ns, time_ns=time_ns,
output_bytes=output_bytes,
timestamp=timestamp timestamp=timestamp
)) ))
elif len(matches) == 2: elif len(matches) == 2:
@ -1036,6 +1048,7 @@ class CargoAOCRunner:
day=day, day=day,
part=idx, part=idx,
time_ns=time_ns, time_ns=time_ns,
output_bytes=output_bytes,
timestamp=timestamp timestamp=timestamp
)) ))
break break
@ -1100,6 +1113,7 @@ class HTMLGenerator:
user = result['user'] user = result['user']
runner_time_ns = result['time_ns'] runner_time_ns = result['time_ns']
generator_time_ns = result.get('generator_time_ns', 0) generator_time_ns = result.get('generator_time_ns', 0)
output_bytes = result.get('output_bytes', 0)
git_rev = result.get('git_rev', '') git_rev = result.get('git_rev', '')
repo_url = result.get('repo_url', '') repo_url = result.get('repo_url', '')
@ -1111,11 +1125,12 @@ class HTMLGenerator:
'total': total_time_ns, 'total': total_time_ns,
'runner': runner_time_ns, 'runner': runner_time_ns,
'generator': generator_time_ns, 'generator': generator_time_ns,
'output_bytes': output_bytes,
'git_rev': git_rev, 'git_rev': git_rev,
'repo_url': repo_url 'repo_url': repo_url
} }
html = self._generate_html(data, years, users, db, config) html = self._generate_html(data, years, users, db, config, results)
output_file = self.output_dir / 'index.html' output_file = self.output_dir / 'index.html'
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
@ -1286,7 +1301,7 @@ class HTMLGenerator:
html += '</div>' html += '</div>'
return html return html
def _generate_html(self, data: dict, years: List[int], users: List[str], db: Database, config: Config) -> str: def _generate_html(self, data: dict, years: List[int], users: List[str], db: Database, config: Config, results: List[dict]) -> str:
"""Generate HTML content""" """Generate HTML content"""
# Get refresh interval from config (default 5 minutes = 300 seconds) # Get refresh interval from config (default 5 minutes = 300 seconds)
refresh_interval = config.config.get('html_refresh_interval', 300) refresh_interval = config.config.get('html_refresh_interval', 300)
@ -1944,6 +1959,60 @@ class HTMLGenerator:
</div> </div>
""" """
# Add output bytes summary table
# Use the results passed to the method
html += """
<div class="summary">
<h3>Output Bytes Summary</h3>
<p style="font-size: 0.9em; color: #666; margin-bottom: 10px;">Number of bytes written to stdout for each day/part/user combination</p>
<table>
<thead>
<tr>
<th>Year</th>
<th>Day</th>
<th>Part</th>
"""
for user in sorted(users):
html += f" <th>{user}</th>\n"
html += """ </tr>
</thead>
<tbody>
"""
# Organize output bytes by year/day/part/user
output_bytes_data = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
for result in results: # results is passed as parameter to _generate_html
year = result['year']
day = result['day']
part = result['part']
user = result['user']
output_bytes = result.get('output_bytes', 0)
# Store output_bytes even if 0, but we'll show "-" for 0 in the table
output_bytes_data[year][day][part][user] = output_bytes
# Generate table rows - use the same structure as main data tables
for year in sorted_years:
if year not in data:
continue
for day in sorted(data[year].keys()):
for part in sorted(data[year][day].keys()):
html += f""" <tr>
<td>{year}</td>
<td>{day}</td>
<td>{part}</td>
"""
for user in sorted(users):
bytes_val = output_bytes_data[year][day][part].get(user, None)
if bytes_val is not None and bytes_val > 0:
html += f" <td>{bytes_val:,}</td>\n"
else:
html += " <td>-</td>\n"
html += " </tr>\n"
html += """ </tbody>
</table>
</div>
"""
# Add summary statistics at the bottom # Add summary statistics at the bottom
html += f""" html += f"""
<div class="summary"> <div class="summary">