Compare commits
4 Commits
c851648166
...
5cc1460a3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc1460a3e | |||
| c604da12c7 | |||
| dde52bb9db | |||
| 171fdb4329 |
188
aocsync.py
188
aocsync.py
@ -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)
|
||||||
@ -1303,6 +1318,19 @@ class HTMLGenerator:
|
|||||||
for part in day.values():
|
for part in day.values():
|
||||||
users_with_data.update(part.keys())
|
users_with_data.update(part.keys())
|
||||||
|
|
||||||
|
# Calculate stars (completed parts) per user per year
|
||||||
|
stars_by_user_year = defaultdict(lambda: defaultdict(int))
|
||||||
|
for year in data:
|
||||||
|
for day in data[year].values():
|
||||||
|
for part in day.values():
|
||||||
|
for user, time_data in part.items():
|
||||||
|
if isinstance(time_data, dict):
|
||||||
|
total_time = time_data.get('total', 0)
|
||||||
|
else:
|
||||||
|
total_time = time_data if time_data > 0 else 0
|
||||||
|
if total_time > 0:
|
||||||
|
stars_by_user_year[user][year] += 1
|
||||||
|
|
||||||
# Check if log file exists
|
# Check if log file exists
|
||||||
log_file_path = Path(config.output_dir) / 'cargo-aoc.log'
|
log_file_path = Path(config.output_dir) / 'cargo-aoc.log'
|
||||||
log_file_exists = log_file_path.exists()
|
log_file_exists = log_file_path.exists()
|
||||||
@ -1673,6 +1701,37 @@ class HTMLGenerator:
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.collapsible-header {{
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.collapsible-header:hover {{
|
||||||
|
opacity: 0.8;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.collapsible-arrow {{
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.collapsible-arrow.expanded {{
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.collapsible-content {{
|
||||||
|
display: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.collapsible-content.expanded {{
|
||||||
|
display: block;
|
||||||
|
}}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
function showHistory(user, year, day, part) {{
|
function showHistory(user, year, day, part) {{
|
||||||
@ -1691,6 +1750,17 @@ class HTMLGenerator:
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
function toggleCollapsible(element) {{
|
||||||
|
const content = element.nextElementSibling;
|
||||||
|
const arrow = element.querySelector('.collapsible-arrow');
|
||||||
|
if (content && content.classList.contains('collapsible-content')) {{
|
||||||
|
content.classList.toggle('expanded');
|
||||||
|
if (arrow) {{
|
||||||
|
arrow.classList.toggle('expanded');
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
// Close modal when clicking outside of it
|
// Close modal when clicking outside of it
|
||||||
window.onclick = function(event) {{
|
window.onclick = function(event) {{
|
||||||
if (event.target.classList.contains('modal')) {{
|
if (event.target.classList.contains('modal')) {{
|
||||||
@ -1720,6 +1790,43 @@ class HTMLGenerator:
|
|||||||
<p><strong>Users:</strong> """ + ', '.join(sorted(users)) + """</p>
|
<p><strong>Users:</strong> """ + ', '.join(sorted(users)) + """</p>
|
||||||
""" + (f'<p><a href="cargo-aoc.log" target="_blank">📋 View Cargo AOC Logs</a></p>' if log_file_exists else '') + """
|
""" + (f'<p><a href="cargo-aoc.log" target="_blank">📋 View Cargo AOC Logs</a></p>' if log_file_exists else '') + """
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stars Summary Table -->
|
||||||
|
<div class="summary" style="margin-top: 20px; margin-bottom: 20px;">
|
||||||
|
<h3>⭐ Stars Summary</h3>
|
||||||
|
<!--<p style="font-size: 0.9em; color: #666; margin-bottom: 10px;">Number of completed parts (stars) per user per year</p>-->
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
"""
|
||||||
|
# Add year columns
|
||||||
|
for year in sorted_years:
|
||||||
|
html += f" <th>{year}</th>\n"
|
||||||
|
html += """ <th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
"""
|
||||||
|
# Add rows for each user
|
||||||
|
for user in sorted(users):
|
||||||
|
html += f""" <tr>
|
||||||
|
<td><strong>{user}</strong></td>
|
||||||
|
"""
|
||||||
|
total_stars = 0
|
||||||
|
for year in sorted_years:
|
||||||
|
stars = stars_by_user_year[user][year]
|
||||||
|
total_stars += stars
|
||||||
|
if stars > 0:
|
||||||
|
html += f" <td>{stars} ⭐</td>\n"
|
||||||
|
else:
|
||||||
|
html += " <td class=\"no-data\">-</td>\n"
|
||||||
|
html += f" <td><strong>{total_stars} ⭐</strong></td>\n"
|
||||||
|
html += " </tr>\n"
|
||||||
|
|
||||||
|
html += """ </tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Generate content for each year (sorted descending)
|
# Generate content for each year (sorted descending)
|
||||||
@ -1944,6 +2051,65 @@ class HTMLGenerator:
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Add output bytes summary table
|
||||||
|
# Use the results passed to the method
|
||||||
|
html += """
|
||||||
|
<div class="summary">
|
||||||
|
<h3 class="collapsible-header" onclick="toggleCollapsible(this)">
|
||||||
|
<span class="collapsible-arrow">▶</span>
|
||||||
|
Output Bytes Summary
|
||||||
|
</h3>
|
||||||
|
<div class="collapsible-content">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
# Add summary statistics at the bottom
|
# Add summary statistics at the bottom
|
||||||
html += f"""
|
html += f"""
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user